本文最后更新于:2020年9月16日 晚上

Java并发系列(十四)-双重检查锁定与延迟初始化

本篇的主要内容:

  • 1、延迟初始化
  • 2、双重检查锁定的过程
  • 3、线程安全的延迟初始化使用建议

1、延迟初始化

1.1、目的

降低初始化类和创建对象的开销。(只有用的时候才会对对象初始化)

1.2、常见的实现方法

双重检查锁定

2、双重检查锁定的过程

1.第一次检查
2.加锁(防止在多线程环境下,只有一个线程创建对象)
3.第二次检查
4.初始化对象
public class DoubleCheckedLocking {                      //1
    private static Instance instance;                    //2

    public static Instance getInstance() {               //3
        if (instance == null) {                          //4: 第一次检查 
            synchronized (DoubleCheckedLocking.class) {  //5: 加锁 
                if (instance == null)                    //6: 第二次检查 
                    instance = new Instance();           //7: 问题的根源出在这里 
            }                                            //8
        }                                                //9
        return instance;                                 //10
    }                                                    //11
}                                                        //12

2.1、问题的根源

问题描述:

在上面实例代码第7步可以分解为3步,其中可能会发生重排序的问题,
另一个线程进来初次判断的时候就可能不为null。

创建对象的步骤:

1.分配对象的内存空间
2.初始化对象
3.设置instance指向刚分配的内存空间

2.2、问题的解决方案

  • 不允许2和3重排序。
  • 允许2和3重排序,但不允许其他线程“看到”这个重排序。

2.2.1、基于volatile的解决方案

这种操作是通过禁止【初始化对象】和【设置instance指向内存空间】这两个操作重排序,
来保证线程安全的延迟初始化。(JDK5以上)

2.2.2、基于类初始化的解决方案

2.2.2.1、一个类或接口类型T立即被初始化的情况
  • T是一个类,T类型的实例被创建时。
  • T是一个类,T中的静态方法被调用。
  • T中声明的一个静态字段被赋值。
  • T中声明的一个静态字段被使用,而且这个字段不是一个常量字段。
  • T是顶级类,并且一个断言语句嵌套在T内部内执行。
2.2.2.2、Java初始化类和接口的处理过程
前提:

    每一个类或接口,都唯一与之对应一个初始化锁。(但它们之间的映射,是由JVM去实现的)
第一阶段
- 第一阶段过程:

    通过在Class对象上同步(即Class对象获取初始化锁),来控制接口和类的初始化。
    没有获取锁的线程会一直等待,直到获取到了初始化锁。

- 执行时序:

    线程A获取到了初始化锁,发现没有被初始化,则将state == noInitialization,线程A释放初始化锁。

    线程B尝试获取初始化锁,无法获取,会一直等待。
第二阶段
- 第二阶段过程:

    线程A执行类的初始化,同时线程B在初始化锁的condition上等待。

- 执行时序:

    线程A进行静态初始化。

    - 线程B获取初始化锁
    - 读取state = initializing
    - 释放初始锁
    - 在初始化锁的condition中等待。
第三阶段
- 第三阶段过程:

    线程A设置state = initialized,然后唤醒在condition中等待的所有线程。

- 执行时序:

    只有线程A执行:
        - 获取初始化锁
        - 设置 state = initialized
        - 唤醒在condition中等待的所有线程
        - 释放初始化锁
        - 线程A的初始化处理过程完成
第四阶段
- 第四阶段过程:

    线程B结束类的初始化处理。

- 执行时序:

    只有线程B执行:
        - 获取初始化锁
        - 读取到state = initialized
        - 释放初始化锁
        - 线程B的类初始化过程完成 

这里存在happens-before关系,之前线程A执行的类的初始化时的写入操作
(执行类的静态初始化和初始化类中声明的静态字段),线程B一定能看到
第五阶段
- 第五阶段过程:

    线程C执行类的初始化的过程

- 执行时序:

    线程C
    - 获取初始化锁
    - 读取到state = initialized
    - 释放初始化锁

这里存在happens-before关系,线程A执行类的初始化时的写入操作,线程C一定能看到。

2.3、线程安全的延迟初始化使用建议

对实例字段使用基于volatile的延迟初始化方案;

对惊天字段使用基于类初始化的方案。

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!博客中转载文章会注明出处,若有版权问题,请及时与我联系!谢谢!

Java并发系列(十五)-Java内存模型综述 上一篇
Java并发系列(十三)-happens-before 下一篇