Java多线程编程(双重检查锁定的问题以及解决方案)
来源:
Java
并发编程的艺术
1 典型例子
双重检查锁定的典型例子,如下:
public class DoubleCheckedLocking { private static Instance instance; /**
* 双重检查锁定
*/ public static Instance getInstance() { if (instance == null) { // 1、第一次检查 synchronized (DoubleCheckedLocking.class) { // 2、加锁 if (instance == null) { // 3、第二次检查 instance = new Instance(); // 4、创建对象 (问题的根源在这里) } } } return instance; } }
这样的双重检查锁定是一个错误的优化!!!
当线程读取到instance
不为null
的时候,instance
引用的对象有可能还没有完成初始化。
2 问题的根源
问题的根源在于 4、创建对象
的流程可能会被重排序。
- 创建对象的一般流程
创建对象的流程可以简化为三步:分配内存空间、初始化对象、设置引用指向分配的内存地址。
memory = allocate(); // 1、分配对象的内存空间 initObject(memory); // 2、初始化对象 instance = memory; // 3、设置instance指向分配的内存地址
其中,2 和 3 之间可能被重排序。
- 重排序之后的流程
memory = allocate(); // 1、分配对象的内存空间 instance = memory; // 2、设置instance指向分配的内存地址 initObject(memory); // 3、初始化对象
那么,在发生重排序的情况下,A
、B
两个线程执行这段双重检查锁定的代码时的执行顺序可能是这样的:
时间 | A 线程 | B 线程 |
---|---|---|
t1 | A1:分配对象的内存空间 | |
t2 | A2:设置instance指向内存空间的地址 | |
t3 | B1:判断instance是否为null | |
t4 | B2:由于instance不是null,B线程将直接访问instance引用的对象 | |
t5 | A3:初始化对象 | |
t6 | A4:访问instance引用的对象 |
按照以上的顺序,B
线程将会在instance
对象还未初始化完成之前就访问它,导致NPE
。
3 解决方案
3.1 基于volatile的解决方案
对于双重检查锁定来实现的延迟初始化方案,只需要将instance
声明为volatile
类型,就能够实现线程安全的延迟初始化:
public class SafeDoubleCheckedLocking { private volatile static Instance instance; // 声明为volatile类型 /**
* 双重检查锁定
*/ public static Instance getInstance() { if (instance == null) { // 1、第一次检查 synchronized (DoubleCheckedLocking.class) { // 2、加锁 if (instance == null) { // 3、第二次检查 instance = new Instance(); // 4、创建对象 } } } return instance; } }
volatile
通过禁止重排序(初始化对象和设置instance
指向分配的内存地址)来保证线程安全的延迟初始化。
3.2 基于类初始化的解决方案
Java
语言规范规定,对于每一个类或者接口,都有唯一的一个初始化锁与之对应。在多线程环境下,只有一个线程能够获取这个锁并执行类的初始化,其他线程需要等待获取这个锁,这样就能保证线程安全的类的初始化。
初始化一个类,包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。
public class InstanceFactory { private static class InstanceHolder { public static Instance instance = new Instance(); } public Instance getInstance() { return InstanceHolder.instance; // 触发InstanceHolder类的初始化 } }
如上,当线程执行getInstance()
方法时,将会触发InstanceHolder
类的初始化,此时只有一个线程能够获取InstanceHolder
类的初始化锁,并完成类的初始化过程,类中的静态字段instance
将被初始化,Instance
对象将被创建。虽然Instance
对象创建过程中存在重排序,但是对于其他线程来说都是不可见的。
4 总结
- 大多数情况下,正常的初始化优于延迟初始化。
-
基于
volatile
的延迟初始化方案有一个优势,就是除了可以对静态字段实现延迟初始化之外,还可以对实例字段实现延迟初始化。 -
如果确实需要对实例字段实现线程安全的延迟初始化,请使用基于
volatile
的延迟初始化方案。 - 如果确实需要对静态字段实现线程安全的延迟初始化,请优先使用基于类初始化的方案。
本文地址:https://blog.csdn.net/hbtj_1216/article/details/108801033