欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java多线程编程(双重检查锁定的问题以及解决方案)

程序员文章站 2022-03-10 15:25:49
双重检查锁定的典型例子,如下:public class DoubleCheckedLocking { private static Instance instance; /** * 双重检查锁定 */ public static Instance getInstance() { if (instance == null) { // 1、第一次检查 synchroniz...

来源: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、初始化对象 

那么,在发生重排序的情况下,AB两个线程执行这段双重检查锁定的代码时的执行顺序可能是这样的:

时间 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