单例模式
单例模式是最简单的设计模式之一,它属于创建型设计模式,提供了一种创建对象的方法。这种模式有一个单例类,它可以创建自己的对象,并且确保只有一个对象被创建(也就是构造方法私有)。并且这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象。
总结就是三点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
使用场景
当想要控制实例数目(表示全局唯一类),节省系统资源,处理资源访问冲突的时候可以使用单例模式,它主要解决一个全局使用的类频繁的创建和销毁,是通过系统是否已经有这个单例,如果有就返回,没有就创建这样的方法来解决的。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。细分下来有以下几点:
- 单例对 OOP 特性的支持不友好,对其中的抽象、继承、多态都支持得不好
- 单例会隐藏类之间的依赖关系
- 单例对代码的扩展性不友好,当需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动
- 单例对代码的可测试性不友好
- 单例不支持有参数的构造函数
单例模式的实现
单例类的实现有线程安不安全的两种懒汉式,饿汉式,双检锁,匿名内部类,枚举这6种:
1、线程不安全的懒汉式:
public class Singleton {
private static Singleton instance; //其属性就包括自己这个类
private Singleton (){} //构造方法私有
public static Singleton getInstance() {//提供直接访问其唯一对象的方式
if (instance == null) {
instance = new Singleton(); //第一次访问才创建自己的对象
}
return instance;
}
}
这种方式支持懒加载(第一次调用才初始化,避免内存浪费),实现也很容易。但最大的问题是线程不安全,也就是不支持多线程,因为是因为访问方法没有加锁 synchronized。
举个例子:我们假设有多个线程1,线程2都需要使用这个单例对象。而恰巧,线程1在判断完s==null后突然交换了cpu的使用权,变为线程2执行,由于s仍然为null,那么线程2中就会创建这个Singleton的单例对象。之后线程1拿回cpu的使用权,而正好线程1之前暂停的位置就是判断s是否为null之后,创建对象之前。这样线程1又会创建一个新的Singleton对象。
2、线程安全的懒汉式:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
它与线程不安全的懒汉式唯一的区别就是访问方法加了synchronized,这样就能在多线程情况下工作了,但加锁了就会导致其效率很低。
3、饿汉式:
public class Singleton {
private static Singleton instance = new Singleton(); //没访问之前就已经初始化了
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
饿汉式,没访问之前就已经初始化单例了,容易产生垃圾对象,这也是它的缺点所在。
优点是没有加锁和早创建单例使得其执行效率提高。同时因为instance在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例。就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此饿汉式单例天生就是线程安全的。
4,双检锁(DCL,即 double-checked locking)
public class Singleton {
private volatile static Singleton singleton; //(给singleton对象加了volatile 关键字)
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
//可能多个线程同时进入这里
synchronized (Singleton.class) { //(给整个类加了锁,以下的为同步代码块)线程1到这里继续往下执行,其他线程等待
if (singleton == null) { //线程1到这里发现singleton 为空,继续执行if代码块
singleton = new Singleton();
/**
* 由于编译器的优化、JVM的优化、操作系统处理器的优化,可能会导致指令重排(happen-before规则下的指令重排,执行结果不变,指令顺序优化排列)
* new Singleton3()这条语句大致会有这三个步骤:
* 1.在堆中开辟对象所需空间,分配内存地址
* 2.根据类加载的初始化顺序进行初始化
* 3.将内存地址返回给栈中的引用变量(instance变量被分配了地址,就不为null了)
* 但是由于指令重排的出现,这三条指令执行顺序会被打乱,可能导致3的顺序和2调换
*/
}
}
}
return singleton;
}
}
双检锁可以说是懒汉式的升级版,支持懒加载,它既是线程安全的,同时又能保证高性能。
为什么singleton对象前要加volatile 关键字?
如果没加volatile 关键字的话,在多线程情况下,由于指令重排导致3,2的顺序调换,会导致以下问题的出现:首先第一个线程执行到了3号指令(instance变量被分配了地址,不为null了),但对象未初始化。此时!第一个if语句进行判断时结果为true,自然而然在使用instance时会出错。解决的方法便是在instance变量上加上volatile关键字,加上volatile关键字后会禁止该变量的指令重排,从而达到线程安全
为什么要有两次if判断?
内层判断:当多个线程同时进入到第一层if代码块内时,线程1到第七行(同步代码块)继续往下执行,其他线程等待,线程1到这里发现singleton 为空,继续执行if代码块实例化Singleton对象,执行完成后退出同步代码块,然后线程2进入第七行内,如果在这里不加内层判断,就会造成instance再次实例化,这就违背了单例模式的单例二字。由于这里加了判断,线程2在这里发现singleton 已经被实例化了就跳出if代码块了。
外层判断:在多个线程依次调用了getInstace函数时,当线程1走完了内层判断,对象实例化了,这个时候线程3也调用了getInstace函数,如果没有加外层的判断,线程3还是要继续等待线程2的完成,而加上外层判断,就不需要等待了,直接返回了实例化的对象。
我的理解:外层的判断是为了提高效率,里层的判断就是第一次实例化需要。
那么学到这里就有一个问题了!支持懒加载的的双检锁是不是就要比饿汉式更优呢?
可能有人觉得饿汉式不好,因为不支持延迟加载,如果实例占用资源多或初始化耗时长,提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过,我并不认同这样的观点。
如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。
如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错,我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。
所以说:支持懒加载的双重检测不比饿汉式更优。
5,静态内部类
public class Singleton {
private static class SingletonHolder { //静态内部类实例化对象
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() { 如果没有到这里,那么不会加载上面的内部类
return SingletonHolder.INSTANCE;
}
}
静态内部类线程安全,支持懒加载(能达到双检锁方式一样的功效,但实现更简单):外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。具体来说当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,使用INSTANCE的时候,才会导致虚拟机加载SingleTonHoler类。这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么具体是如何实现线程安全的?
首先类加载过程中的最后一个阶段:即类的初始化,它的本质就是执行类构造器的clinit方法。JVM内部会保证一个类的clinit方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的clinit方法,其他的线程都要阻塞等待,直到这个线程执行完clinit方法。然后执行完clinit方法后,其他线程唤醒,但是不会再进入clinit方法。也就是说同一个加载器下,一个类型只会初始化一次。
回到这个代码中,这里的静态变量的赋值操作进行编译之后实际上就是一个clinit代码,当我们执行getInstance方法的时候,会导致SingleTonHolder类的加载,类加载的最后会执行类的初始化,但是即使在多线程情况下,这个类的初始化的代码也只会被执行一次,所以他只会有一个实例。
6、枚举
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
这种实现方式还没有被广泛采用,在effective java这本书中说道,最佳的单例实现模式就是枚举模式,它更简洁,利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题,自动支持序列化机制。
不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
总结:一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种静态内部类。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。
单例有什么替代解决方案?
为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。不过,静态方法这种实现思路,并不能解决我们之前提到的问题。如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类了。比如,通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,通过程序员己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。
模式没有对错,关键看你怎么用。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局的类,我们在其他地方 new 的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便