Sub-item
type
status
date
slug
summary
tags
category
icon
password
Parent item
日期
Jan 23, 2024 03:10 AM
目录
目录设计模式的六大原则常见的Java设计模式单例模式懒加载 (Lazy Loading)饿汉模式(Eager Singleton)懒汉模式(Lazy Singleton)(非线程安全)线程不安全,禁止使用懒汉模式(Lazy Singleton)(线程安全)效率低不建议使用双重校验锁(Double-Checked Locking Singleton)(推荐)静态内部类(Static Inner Class Singleton)枚举参考
设计模式的六大原则
- 单一职责原则(Single Responsibility Principle,SRP) 一个类应该只有一个引起它变化的原因,也就是说,一个类只负责一件事情。如果一个类承担的职责过多,就会变得复杂且难以维护。
- 开闭原则(Open-Closed Principle,OCP) 对于扩展开放,对于修改关闭。也就是说,在不修改已有代码的情况下,可以通过扩展来增加新的功能。
- 里氏替换原则(Liskov Substitution Principle,LSP) 任何一个子类应该可以被它的父类替换,而不会产生任何不良影响。也就是说,子类不能改变父类原有的功能。
- 接口隔离原则(Interface Segregation Principle,ISP) 使用多个专门的接口,而不是一个通用的接口,以避免客户端依赖于它们不需要的方法。接口应该是小而专一的。
- 依赖倒置原则(Dependency Inversion Principle,DIP) 高层模块不应该依赖低层模块,两者应该依赖于抽象。抽象不应该依赖于具体实现细节,而具体实现细节应该依赖于抽象。
- 迪米特法则(Law of Demeter,LoD) 也称作最少知识原则,一个对象应该对其他对象保持最少的了解。也就是说,一个类应该尽可能地降低与其他类的耦合度。
常见的Java设计模式
1.单例模式:
单例模式是一种创建型模式,它保证一个类只有一个实例,并提供一个访问它的全局访问点。
2.工厂模式:
工厂模式是一种创建型模式,它定义了一个接口用于创建对象,但是让子类决定实例化哪一个类。工厂方法让类把实例化推迟到子类。
3.观察者模式:
观察者模式是一种行为型模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当主题对象状态发生改变时,它的所有依赖者都会收到通知并自动更新。
4.建造者模式:
建造者模式是一种创建型模式,它将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
5.策略模式:
策略模式是一种行为型模式,它定义了一系列的算法,将每个算法都封装起来,并使它们可以相互替换,使得算法的变化独立于使用它们的客户端。
6.适配器模式:
适配器模式是一种结构型模式,它将一个类的接口转换成客户端希望的另一个接口。适配器模式使得原本由于接口不兼容而不能在一起工作的类可以在一起工作。
7.装饰者模式:
装饰者模式是一种结构型模式,它允许向一个现有的对象添加新的功能,同时又不改变其结构。
8.迭代器模式:
迭代器模式是一种行为型模式,它提供一种访问一个聚合对象中各个元素,而又不需要暴露该对象的内部表示的方法。
单例模式
单例模式是设计模式中使用最为普遍的一种模式。属于对象创建模式,它可以确保系统中一个类只产生一个实例。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
这样的行为能带来两大好处:
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。
- 由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。
注意:
1、单例类只能有一个实例,以私有构造函数,或者内部类形式。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
4、指一个类仅有一个实例,并提供一个访问它的全局访问点。简单的讲就是一个类只能创建一个实例,即使创建了多个实例,这些实例都是相同的。不会出现被覆盖的问题,保证只有一个实例,并提供全局访问。
优点:
- 确保只有一个实例存在,避免多个实例引起的资源浪费。
- 提供一个全局访问点来访问该实例,方便在整个应用程序中共享数据和状态。
- 避免因为多个实例之间的状态不同步导致的错误(比如写文件操作)。
- 可以在单例类中实现一些全局性的操作,例如记录日志、统计数据等。
缺点:
- 单例类的扩展性差,如果需要增加新的功能,可能需要修改原有的代码。
- 单例类对于测试不太友好,因为单例类无法被实例化,也就难以进行单元测试。
- 单例类的设计可能会破坏面向对象的封装性原则,因为单例类的状态是全局共享的,可能会被其他类随意修改。
- 单例模式可能会导致代码的耦合度增加,因为单例类在全局范围内被访问,可能会与其他模块产生依赖关系。
因此,使用单例模式需要仔细考虑其适用性和风险,特别是在多线程和分布式环境下需要特别注意线程安全问题。
使用场景:
- 方便资源之间的互相通信。如线程池等。
- 避免由于资源操作时导致的性能或损耗等。如数据库连接池等。
- 要求生成唯一序列号的环境。
- 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的。
- 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。
- 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。
- 资源共享的情况:由于单例模式只创建一个实例,因此可以避免多个实例之间的资源竞争问题,保证资源的共享。
- 控制实例的数量:由于单例模式只创建一个实例,因此可以限制实例的数量,避免过多的内存占用。
- 系统设置或全局配置的场景:例如系统中只有一个配置文件或者只有一个日志对象等,这些对象需要被多个模块共享和访问。
- 管理对象或状态的情况:例如数据库连接池、线程池、缓存等对象,这些对象需要被多个模块共享和访问,并且需要对其进行管理。
- 适用于频繁创建和销毁的对象:例如计数器、序列号生成器等对象,这些对象在多个模块中需要频繁创建和销毁,使用单例模式可以避免频繁的创建和销毁操作,提高系统的性能。
在实际应用中,很多时候有一些对象我们只需要一个,例如:线程池(threadpool)、缓存(cache)、注册表(registry)、日志对象等等,这个时候把它设计为单例模式是最好的选择。
懒加载 (Lazy Loading)
“懒加载”也被叫作“延迟价值”,它的核心思想是把对象的实例化延迟到真正调用该对象的时候,这样做的好处是可以减轻大量对象在实例化时对资源的小号,而不是在程序初始化的时候就预先将对象实例化。
另外“懒加载”可以将对象的实例化代码从初始化方法中独立出来,从而提高代码的可读性,以便于代码能够更好地组织。
例如getter方法被重写,使得在第一次调用getter方法时才实例化对象并将实例化的对象返回,并且需要通过判断对象是否为空来防止对象重复实例化。
在单例模式中,懒加载(Lazy Loading)是指在需要使用单例实例时才进行实例化,而不是在程序启动时就立即创建单例实例。懒加载可以减少程序启动时的开销,提高程序性能。
懒加载有多种实现方式,其中比较常见的方式是双重校验锁(Double-Checked Locking)和静态内部类(Static Inner Class)。
饿汉模式(Eager Singleton)
从代码中我们看到,类的构造函数定义为private的,保证其他类不能实例化此类,然后提供了一个静态实例并返回给调用者。
饿汉模式是最简单的一种实现方式,饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。
它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。
它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
优点:
- 线程安全,不需要考虑多线程同步的问题。
- 在程序启动时就立即创建单例实例,避免了延迟加载的性能问题。
缺点:
- 在程序启动时就创建单例实例,可能会浪费一些资源。
- 没有实现懒加载,如果单例实例从未使用,就会浪费资源。
懒汉模式(Lazy Singleton)(非线程安全)线程不安全,禁止使用
如上,通过提供一个静态的对象instance,利用private权限的构造方法和getInstance()方法来给予访问者一个单例。
缺点是,没有考虑到线程安全,可能存在多个访问者同时访问,并同时构造了多个对象的问题。之所以叫做懒汉模式,主要是因为此种方法可以非常明显的lazy loading。
这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。
因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
优点:达到懒加载,只有在需要使用单例实例时才进行实例化。延迟创建单例实例,避免了饿汉式单例模式的资源浪费问题。
缺点:线程不安全。
懒汉模式(Lazy Singleton)(线程安全)效率低不建议使用
然而并发其实是一种特殊情况,大多时候这个锁占用的额外资源都浪费了,这种打补丁方式写出来的结构效率很低。
优点:达到懒加载,只有在需要使用单例实例时才进行实例化。延迟创建单例实例,避免了饿汉式单例模式的资源浪费问题。
缺点:线程并不完全安全,加载效率低。
这里解释一下为什么懒汉模式还不完全安全?
在多线程环境下,如果两个线程同时判断instance是否为null,都发现instance为null,就有可能导致两个线程都创建Singleton实例,并且各自赋值给instance变量,这样就会破坏单例模式的要求。例如,假设有两个线程T1和T2,都调用Singleton.getInstance()方法,此时instance变量为null,线程T1执行了instance = new Singleton()语句,创建了一个Singleton实例并赋值给instance变量,但是还没有执行完构造函数,此时线程T2也判断instance为null,并且也执行了instance = new Singleton()语句,创建了一个新的Singleton实例并赋值给instance变量。这样就会导致两个Singleton实例的存在,从而破坏了单例模式的要求。为了解决这个问题,可以在getInstance方法中增加同步措施,确保只有一个线程能够同时执行到创建Singleton实例的代码。或者使用双重检查锁单例模式(Double-Checked Locking Singleton)或者静态内部类单例模式(Static Inner Class Singleton)等其他安全的单例模式实现方式。
双重校验锁(Double-Checked Locking Singleton)(推荐)
接下来我解释一下在并发时,双重校验锁法会有怎样的情景:
STEP 1. 线程A访问getInstance()方法,因为单例还没有实例化,所以进入了锁定块。
STEP 2. 线程B访问getInstance()方法,因为单例还没有实例化,得以访问接下来代码块,而接下来代码块已经被线程1锁定。
STEP 3. 线程A进入下一判断,因为单例还没有实例化,所以进行单例实例化,成功实例化后退出代码块,解除锁定。
STEP 4. 线程B进入接下来代码块,锁定线程,进入下一判断,因为已经实例化,退出代码块,解除锁定。
STEP 5. 线程A初始化并获取到了单例实例并返回,线程B获取了在线程A中初始化的单例。
理论上双重校验锁法是线程安全的,并且,这种方法实现了lazyloading。
优点:
- 结合了饿汉式和懒汉式的优点,既能够保证线程安全,又能够延迟创建单例实例。
- 在getInstance方法中使用了同步锁,避免了每次调用getInstance方法都需要同步的性能问题。
缺点:实现相对复杂,需要考虑多线程同步和延迟加载的问题。
静态内部类(Static Inner Class Singleton)
这种方式当
Singleton
类被加载时,其内部类并不会被加载,所以单例类INSTANCE
不会被初始化。
只有显式调用getInstance
方法时,才会加载SingletonHolder
,从而实例化INSTANCE
。由于实例的建立是在类加载时完成,所以天生线程安全。因此兼备了懒加载和线程安全的特性。
使用内部类的好处是,静态内部类不会在单例加载时就加载,而是在调用getInstance()方法时才进行加载,达到了类似懒汉模式的效果,而这种方法又是线程安全的。
优点:
- 利用了类加载机制,保证了线程安全和单例实例的唯一性。
- 延迟加载,只有在需要使用单例实例时才进行实例化。
- 实现简单、安全,代码优雅。
缺点:
- 需要理解类加载机制,有一定的学习曲线。
枚举
Effective Java作者Josh Bloch 提倡的方式,在我看来简直是来自神的写法。解决了以下三个问题:
(1)自由序列化。
(2)保证只有一个实例。
(3)线程安全。
如果我们想调用它的方法时,仅需要以下操作:
这种充满美感的代码真的已经终结了其他一切实现方法了。
优点:装的一手好逼。避免了线程不安全,延迟加载,效率高。 如果面试中出现让你手写代码要去写后三种,最好写双重检查。
参考
- 作者:fighting-bug
- 链接:https://www.fighting-bug.top//post/DesignPattern
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。