[Java并发] 单例模式下双重锁定 问题和解决方案
来自阅读Java并发编程的艺术读书笔记,按照自己思路写了一些整理,综合了网上的一些博文和资料
本篇来源于Java并发编程的艺术 Ch3.8 内容
同样是一段总结:
- DCL-Double Check Lock进行初始化动作,可能因重排序(构造-赋值到实例引用)造成线程不安全问题
- 通过Volatile-instance禁止重排序(Volatile用Barrier来禁止写操作与之前操作的重排序)
有些麻烦,但是可以同时初始化对象的字段- 通过内部类初始化的方法(内部类初始化可以重排序,但是对其他线程不可见)
双重检查锁定 DCL 与延迟初始化
众所周知,单例模式中经常使用DCL双重检查锁定来降低同步的开销:
但是这种做法不一定安全。示例代码的第七行可以分解为如下三行:
// instance = new Instance()
memory = allocate(); // 1. 分配对象内存空间
ctorInstance(memory); // 2. 初始化对象
instance = memory; // 3. 设置instance到刚才分配的地址
在代码2/3
之间,可能被重排序。
对于单线程程序来讲,虽然2/3重排序了,但只要保证2在4的前面执行,单线程内的结果就不会被改变:
但是在多线程场景下,可能出现问题:线程B在3-2之间插入,发现instance虽然为非空,但是实际上对象还未被正确初始化
解决方案1:基于Volatile
由于Volatile内存模型中确定,3
作为对Volatile的写操作,而写操作之前的操作不能与写操作进行重排序,于是2/3
之间的重排序被禁止,问题解决
private volatile static Instance instance; // 增加volatile关键字
解决方案2:基于类初始化的懒加载
根据类初始化的过程,对于每一个类或接口C,都必须有唯一的初始化锁LC与之对应。从C到LC的映射,由JVM具体自由实现。JVM类初始化期间会获取这个初始化锁,并且每个线程都至少获取一次初始化所以保证初始化已完成。
通过比较Volatile-instance方案和类初始化懒加载方案,显然类初始化更方便;但是Volatile-instance
配上DCL能够自由定制(对实例的字段进行初始化)