读《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();的执行过程可能被重排序。

正常过程如下:

  1. 分配内存空间
  2. 初始化Singleton实例
  3. 赋值 instance 实例引用

但是被重排序以后可能会出现:

  1. 分配内存空间
  2. 赋值 instance 实例引用
  3. 初始化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与之对应。

总结

方案一,除了可以实现对静态字段的延迟初始化外,还可以实现对实例字段的延迟初始化。
方案二,实现代码更简洁。

字段延迟初始化降低了初始化类或创建实例的开销,但是增加了访问被延迟初始化的字段的开销。

  • 在大多数时候,正常的初始化要优于延迟初始化
  • 如果确实需要对实例字段使用线程安全的延迟初始化,请使用方案一
  • 如果确实需要对静态字段使用线程安全的延迟初始化,请使用方案二
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐