并发编程的艺术之读书笔记(五)

前言:

上一部分,我们一起学习了锁的内存语义和final域的内存语义,这一部分我们一起来学习双重检查锁定和延迟初始化。

1. 双重检查锁的由来

java程序中,有时需要推迟一些高开销的初始化操作,把初始化推迟到使用这些对象的时候才进行,这种时候就要用到延迟初始化。但是延迟初始化如果不使用一些技巧的话很容易产生问题。比如在多线程环境中的延迟初始化,下面举一个非线程安全的延迟初始化对象的例子

public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {        //1:A线程执行
            instance = new Singleton();//2:B线程执行
        }
        return instance;
    }
}

假设有两个线程A和B,线程A执行代码1的同时,线程B执行代码2,那么线程A可能会看到instance引用的对象还未完成初始化。

对于这个类,我们可以对getInstance()方法做同步处理来实现线程安全的初始化

public class Singleton {
    private static Singleton instance;

    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

但是synchronized是重量级锁,性能开销比较大,如果getInstance()方法被多个线程频繁调用产生锁竞争的情况,将会导致性能的下降,所以这时候又有了一种方法,那就是“双重检查锁定”,但是双重检查锁定是个错误的优化,下面来看看双重检查锁定实现延迟初始化的例子

public class Singleton {
    private static Singleton instance;

    public synchronized static Singleton getInstance() {
        if (instance == null) {              //1: 第一次检查
            synchronized (Singleton.class) { //2:加锁
                if (instance == null) {      //3:第二次检查
                    instance=new Singleton();//4:双重检查锁定会出现问题
                }
            }
        }
        return instance;
    }
}

如上面代码所示,如果第一次检查结果不为null,那么就不用执行下面的加锁和初始化操作,直接返回instance对象即可。上面的代码看似很完美,但是实际上在第四步时,代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。

这是为什么呢?实际上在instance=new Singleton();这句代码执行的时候,底层会分解为三行代码

memory=allocate();//1:分配对象的内存空间

ctorInstance(memory)//2:初始化对象

instance=memory;     //3:设置instance指向刚分配的内存空间

上面的3行代码中,第二行和第三行可能会产生重排序的现象,这样的话,这段代码的执行顺序就会变这样

memory=allocate();//1:分配对象的内存空间

instance=memory;     //2:设置instance指向刚分配的内存空间,这时候对象还没被初始化!!!

ctorInstance(memory)//3:初始化对象

由于单线程程序遵循as-if-serial语义,所以重排序不会影响程序最终执行的结果,可是在多线程并发的情况下,线程B在判断instance是否为空的时候,由于2和3被重排序,导致判断结果instance不为空,所以线程B直接返回了instance,但其实instance还没有初始化。对于这个问题,可以有两种办法来解决,第一种就是禁止2和3之间的重排序,第二种是允许2和3之间重排序,但是不允许别的线程“看到”这个重排序。

首先来讲禁止重排序的方法,那就是在instance前加上volatile,就可以实现线程安全的延迟初始化了。

public class Singleton {
    private volatile static Singleton instance;//加上volatile关键字,禁止指令重排序

    public synchronized static Singleton getInstance() {
        if (instance == null) {              //1: 第一次检查
            synchronized (Singleton.class) { //2:加锁
                if (instance == null) {      //3:第二次检查
                    instance=new Singleton();//4:双重检查锁定不会出现问题
                }
            }
        }
        return instance;
    }
}

由于volatile的禁止指令重排序的特性,2和3之间的重排序将被禁止,也就不会发生线程B访问到没有初始化的instance的情况了。

第二种方法是一种基于类初始化的方法,JVM在类初始化阶段(Class被加载后,被线程使用之前),会执行类的初始化,在执行类的初始化期间,JVM会去获取一把锁。这个锁可以同步多个线程对同一个类的初始化。下面用代码来解释一下。

public class Singleton {
    private static class InstanceHolder {
        public static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return InstanceHolder.instance;     //这里将导致InstanceHolder类被初始化
    }
}

假设两个线程并发执行getInstance()方法,示意图如下

初始化一个类,包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。根据java语言规范,在首次发生下列任意一种情况时,一个类或接口类型T将被立即初始化。

  1. T是一个类,而且一个T类型的实例被创建
  2. T是一个类,且T中声明的一个静态方法被调用
  3. T中声明的一个静态字段被赋值
  4. T中声明的一个静态字段被使用,而且这个字段不是一个常量字段
  5. T是一个顶级类,而且一个断言语句嵌套在T内部被执行

在上面的例子里,首次执行getInstance()方法的线程将导致InstanceHolder类被初始化。通过对比volatile实现的双重检查锁定方案和基于类初始化的方案,我们发现基于类初始化的方案代码更加简洁,不过volatile实现的方案有一个好处就是不但可以对静态字段实现延迟初始化,还可以对实例字段实现延迟初始化。

总结

本部分我们一起学习了双重检查锁定和延迟初始化,知道了双重检查锁定的缺点以及解决方式,而且还学习了基于类初始化的延迟初始化方案,下一部分,我们将开始学习java并发编程基础。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值