前言
单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例,比如 JDBC 中的 DataSource 实例就只需要一个。单例模式具体的实现方式有"饿汉" 和 "懒汉" 两种。
1.饿汉模式(类加载的同时创建实例)
class Singleton { // 先创建出示例 private static Singleton instance = new Singleton(); // 如果需要使用这个唯一实例, 统一通过 Singleton.getInstance() 方式来获取. public static Singleton getInstance() { return instance; } // 把构造方法设为 private. 在类外面, 就无法通过 new 的方式来创建这个 Singleton 实例 private Singleton() {} }
此时饿汉模式即使在多线程情况下,也是线程安全的,因为只涉及到读操作。
2.懒汉模式
2.1单线程版的懒汉模式
class SingletonLazy { private static SingletonLazy instance = null; public static SingletonLazy getInstance() { if (instance == null) { instance = new SingletonLazy(); } return instance; } private SingletonLazy() {} }
如果此时这种懒汉模式放在多线程中是不安全的,因为既涉及到了读操作也涉及到了写操作。如果此时多个线程同时都读到了instance == null,那么就会多次进行new操作,这显然就不是单例了。
2.2进行加锁,对单线程版懒汉模式进行改进
class SingletonLazy { private static SingletonLazy instance = null; public static SingletonLazy getInstance() { synchronized (SingletonLazy.class){ if (instance == null) { instance = new SingletonLazy(); } } return instance; } private SingletonLazy() {} }
这样进行加锁之后,就保证了读操作和修改操作是一个整体了。但是,目前进行加锁改进后的懒汉模式代码仍然还有问题,这样的加锁之后,就意味着每一次的getInstance都需要加锁,因为进行加锁操作是需要花费很大开销的,那么每一次都需要进行加锁,会浪费很大的开销。这里的加锁的本意是只在new出对象之前加,是有必要的,一旦new对象完成,后续加锁也就没有意义了,因为instance的值一定是非空的,直接就走return了。所以,还有改进的空间。
2.3继续改进(满足特定条件才进行加锁,也就是第一次new对象的时候才进行加锁)
class SingletonLazy { private static SingletonLazy instance = null; public static SingletonLazy getInstance() { if (instance == null){ synchronized (SingletonLazy.class){ if (instance == null) { instance = new SingletonLazy(); } } } return instance; } private SingletonLazy() {} }
第一层if (instance == null)判断是为了判断是否是第一次new对象,是否需要进行加锁;synchronized (SingletonLazy.class)此处就是在满足了特定条件后才进行加锁,即第一次new对象的时候;第二层 if (instance == null)是判断是否要创建对象。加锁操作可能会引起线程阻塞,当执行到锁结束之后,执行到第二个if 的时候第二个if和第一个if之间可能已经隔了很久的时间,程序的运行内部的状态,这些变量的值,都可能已经发生很大改变了,比如第一次都是null,有很多个线程都进入了第一个if,但是此时在synchronized这里阻塞等待,只有一个能进去,当后面的线程再次进去的时候,一定要靠第二个if来判断,因为此时instance很可能已经被第一次进去的线程给改了,所以很可能不为空,所以还需要第二个if来判断。
但是此时,这段代码还有一些问题,那就是内存可见性问题。比如,同时有很多很多的线程都去getInstance,此时只有一个线程是真正进入了第二个if里面读取了内存,其他在外面的很多线程都是读寄存器/cache,那么此时编译器很可能会认为多次读取到的都是null,有可能会被优化处理。
还有就是在这个过程中,会涉及到指令重排序的问题。此处的instance = new SingletonLazy();可以拆分成三个步骤,(1)申请内存空间,(2)调用构造方法,把这个内存空间初始化成一个合理的对象,(3)把内存空间的地址赋值给instance引用。
在正常情况下,是按照123这个顺序来执行的,但是编译器为了提高程序效率,可能会调整代码的执行顺序,比如把123变成132。
假设t1是按照132的步骤来执行的,当t1执行到13步骤结束,2步骤之前的时候,被切出CPU了,换t2来执行。此时,t1执行完步骤3之后,在t2看来,此处的引用就非空了,此时此刻,t2就直接return了instance引用,并且可能会尝试使用引用中的属性。但是由于t1中的步骤2还没有完成,t2拿到的是非法的对象,还没有构造完成的不完整的对象。
2.4使用volatile关键字来解决内存可见性和禁止指令重排序的问题
class SingletonLazy { private volatile static SingletonLazy instance = null; public static SingletonLazy getInstance() { if (instance == null){ synchronized (SingletonLazy.class){ if (instance == null) { instance = new SingletonLazy(); } } } return instance; } private SingletonLazy() {} }