双重检查锁定(Double-Checked Locking)的问题和解决方案
读《Java并发编程的艺术》方腾飞、魏鹏、程晓明著。笔记一、什么是双重检查锁定为了提高性能,会延迟初始化某些类,在第一次使用的时候做类的初始化。为了保证多线程下的线程安全,一般会做安全同步。简单的方式就是如下:public class Singleton {private static Singleton instance;public synchronized Singleton getInst
读《Java并发编程的艺术》方腾飞、魏鹏、程晓明著。笔记
一、什么是双重检查锁定
为了提高性能,会延迟初始化某些类,在第一次使用的时候做类的初始化。为了保证多线程下的线程安全,一般会做安全同步。简单的方式就是如下:
public class Singleton {
private static Singleton instance;
public synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
对方法添加 synchronized
关键词,每次访问时,就可以同步处理,安全。但是如果 getInstance()
方法调用频繁,每次都要做同步,性能开销会比较大。所以有人提出使用 “ 双重检查锁定(Double-Checked Locking) ”。如下:
public class Singleton {
private static Singleton instance;
public Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这样看似完美解决了问题,但是存在问题。
二、双重检查锁定的问题
假设有两个线程A、B,当线程A 执行到 instance = new Singleton();
时,线程B执行到 if (instance == null)
。这里如果正常,那就是 Singleton被新建,并赋值给 instance ,线程B 拿到instance时不为null,同时开始使用 instance。
但是 instance = new Singleton();
的执行过程可能被重排序。
正常过程如下:
- 分配内存空间
- 初始化Singleton实例
- 赋值 instance 实例引用
但是被重排序以后可能会出现:
- 分配内存空间
- 赋值 instance 实例引用
- 初始化Singleton实例
这样重排序并不影响单线程的执行结果,JVM是允许的。但是在多线程中就会出问题。
当重排序以后,线程B 拿到了不为null 的instance实例引用,但是并没有被初始化,然后线程B使用了一个没有被初始化的对象引用,就出问题了。
三、双重检查锁定的解决方案
问题的根源在于:
- 不允许 2 和 3 发生重排序
- 允许2 和 3 发生重排序,但是不允许其他线程 “ 看到 ” 这个重排序。
3.1 解决方案一:不允许重排序
添加 volatile 关键词防止重排序。
代码如下:
public class Singleton {
// 添加关键词
private volatile static Singleton instance;
public Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
3.2 解决方案二:基于类初始化
代码如下:
public class Singleton {
private static class InstanceHolder{
public static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return InstanceHolder.instance;
}
}
内部类是延迟加载的,只有在第一次使用的时候才被加载。
对于每一个接口和类,在初始化时都有一个唯一的初始化锁LC与之对应。
总结
方案一,除了可以实现对静态字段的延迟初始化外,还可以实现对实例字段的延迟初始化。
方案二,实现代码更简洁。
字段延迟初始化降低了初始化类或创建实例的开销,但是增加了访问被延迟初始化的字段的开销。
- 在大多数时候,正常的初始化要优于延迟初始化。
- 如果确实需要对实例字段使用线程安全的延迟初始化,请使用方案一;
- 如果确实需要对静态字段使用线程安全的延迟初始化,请使用方案二。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)