双重检查锁定(Double-Checked Locking)的问题和解决方案

读《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与之对应。

总结

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

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

  • 在大多数时候,正常的初始化要优于延迟初始化
  • 如果确实需要对实例字段使用线程安全的延迟初始化,请使用方案一
  • 如果确实需要对静态字段使用线程安全的延迟初始化,请使用方案二
  • 21
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值