一、引言
在JAVA多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的 开销。双重检查锁定就是延迟 初始化技术。
二、双重检查锁定的由来
在jav进程中,有时候可能 需要推迟一些高开销对象的初始化操作,并且只有在使用这些对象时才进行初始化,此时,程序员可能会采用延迟初始化。还有一个经典的使用场景就是单利模式下的,为了提高性能 ,采用双重检查锁定模式。但是在使用的过程中我们需要一些技巧,否则很容易出现问题。
上面代码在多线程环境下,我们很容易出现问题,所以改进代码。
由于对getInstance()方法做了同步处理,synchronized将导致性能的开销。如果 getInstance()方法被多个线程频繁调用,将会导致程序执行性能下降。因此,为了提高性能,聪明的程序员想出了一个很好技巧: 双重检查锁定(DCL).继续优化代码
这段代码看起来非常完美,但是这是一个错误的优化,在线程执行到第4行代码的时候,代码渠道instance不为null时,instance引用对象有可能还没有完成初始化。
三、 问题的源
在第七行代码,创建对象时候,我们可以将这一行代码进行分解为下面的伪代码。
这就是我们JVM类加载的三个过程的抽象,如果我们非常熟悉类加载过程,我们应该会很清楚上面的三行伪代码的意思。
为什么将第七行代码拆开来说看呢,因为这三个过程可能会发生指令重排,发生指令重排后可能的执行顺序为:
发生这样的指令重排完全符合规则,因为它重排以后保证了 单线程内程序的执行规则。
这样的执行顺序,在多线程的环境下,就会出现问题了,线程B将看到一个没有初始化的对象。
四、解决方案
通过上面分析,我们知道了问题的原因是指令重排造成的,具体来说是上面的2和3发生重排导致,所以 我们解决问题的关键就是怎么解决重排的问题,下面两个思路
1、不允许2和3重排
2、允许2和3发生重排序,但是不允许其他线程看到这个重排序
基于volatile的解决方案
当声明对象的引用为volatile后,上面的伪代码中的2和3指令之间的重排序在多环境中将会被禁止。
基于类的初始化的解决方案
JVM在类的初始化阶段, 会执行累点 初始化。在执行类的初始化期间,JVM会获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的初始化方案。
假设两个线程并发执行getInstance()方法,下面是执行示意图:
这个方案的实质是,允许2和3重排序,但不允许非构造线程(B线程) 看到这个重排序。