单例模式之双重检查锁(double check locking)的发展历程

it2025-03-15  28

不安全的单例

没有注意过多线程安全问题的时候,我们的单例可能是这样的:

public final class Singleton { private static Singleton instance; private Singleton () { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton (); } return instance; } }

这种写法在单线程中没有问题,但多线程中却会有两个引用对象,可以观察下两个线程调用的情况:

TimeThread AThread BT1检查到instance为空T2检查到instance为空T3初始化对象AT4返回对象AT5初始化对象BT6返回对象B

此使连个线程调用方分别拥有两个对象A、B的实例,就完全不是单例了

解决方法

加锁(synchronized)

public final class Singleton { private static Singleton instance; private Singleton () { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton (); } return instance; } }

在getInstance()方法中增加synchronized,保证了线程安全性,既简单又好理解,但性能不高,因为每次调用getInstance()方法都需要加锁(实际上只需要第一次初始化进行加锁),所以需要针对这个问题进行优化

双重检查锁

public final class Singleton { private static Singleton instance; private Singleton () { } public static Singleton getInstance() { if (null == instance) { synchronized (Singleton.class) { if (null == instance) { instance = new Singleton(); } } } return instance; } }

synchronized加锁仅发生在对象需要实例化的时候,否则其它情况都是直接返回已有的对象。

隐患(指令重排)

instance = new Singleton();这句代码可以分解成三步骤:

分配内存空间初始化对象将对象指向刚分配的内存空间

但有些编译器为了性能原因,可能会将第2步和第3步进行重排序,重排后顺序可能就是:

分配内存空间将对象指向刚分配的内存空间初始化对象

现在考虑重排后,两个线程发生了以下调用:

TimeThread AThread BT1检查到instance为空T2获取锁T3再次检查到instance为空T4为instance分配内存空间T5将instance指向内存空间(重排后的第2步)T6检查到instance不为空T7访问instance(此时对象还未完成初始化)T8初始化instance(重排后的第3步)

这种情况下Thread B访问的是一个还没有初始化的对象。

解决指令重排隐患(最正确的双重锁方式)

public final class Singleton { private volatile static Singleton instance; private Singleton () { } public static Singleton getInstance() { if (null == instance) { synchronized (Singleton.class) { if (null == instance) { instance = new Singleton(); } } } return instance; } }

在instance字段增加volatile后,防止了指令重排,按照预想的执行顺序先初始化对象再指向内存空间,并且所有的写(write)操作都将发⽣在读(read)操作之前

扩展

在对象实例化时的加锁性能可以再进行优化,那就是通过加入局部变量的方式,性能可以提高25%,可以参考《Effective Java, Second Edition》 p. 283-284

public static Singleton getInstance() { // 局部变量将性能提高25%, Singleton result = instance; // 单例双重检查 // 第一重 if (result == null) { synchronized (Singleton.class) { result = instance; // 第二重 if (result == null) { instance = result = new Singleton(); } } } return result; }

总结

通过多个示例可以看出,无论加双重锁、还是加volatile,都是事出有因,只要我们了解了背后的根本原因,就很容易理解为什么要这么写了

如果你喜欢我的文章,记得一键三连(不要下次一定)

最新回复(0)