并发编程的艺术之读书笔记(五)
前言:
上一部分,我们一起学习了锁的内存语义和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;
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将被立即初始化。
- T是一个类,而且一个T类型的实例被创建
- T是一个类,且T中声明的一个静态方法被调用
- T中声明的一个静态字段被赋值
- T中声明的一个静态字段被使用,而且这个字段不是一个常量字段
- T是一个*类,而且一个断言语句嵌套在T内部被执行
在上面的例子里,首次执行getInstance()方法的线程将导致InstanceHolder类被初始化。通过对比volatile实现的双重检查锁定方案和基于类初始化的方案,我们发现基于类初始化的方案代码更加简洁,不过volatile实现的方案有一个好处就是不但可以对静态字段实现延迟初始化,还可以对实例字段实现延迟初始化。
总结
本部分我们一起学习了双重检查锁定和延迟初始化,知道了双重检查锁定的缺点以及解决方式,而且还学习了基于类初始化的延迟初始化方案,下一部分,我们将开始学习java并发编程基础。
上一篇: 实战 | 使用 Python 哄女朋友
下一篇: Maven安装与配置
推荐阅读
-
并发编程(五)——AbstractQueuedSynchronizer 之 ReentrantLock源码分析
-
《C#并发编程经典实例》读书笔记-关于并发编程的几个误解
-
python 之 并发编程(非阻塞IO模型、I/O多路复用、socketserver的使用)
-
Java并发编程的艺术-----Java并发编程基础(线程间通信)
-
《Java 编程思想》读书笔记之并发(一)
-
《Java并发编程的艺术》笔记
-
【响应式编程的思维艺术】 (4)从打飞机游戏理解并发与流的融合
-
<
>-阅读笔记和思维导图 -
Python并发编程之线程中的信息隔离(五)
-
JAVA并发编程(三):同步的辅助类之闭锁(CountDownLatch)与循环屏障(CyclicBarrier)