文章目录
- 一、引言
- 1.1 双重检查锁定(Double-Checked Locking,简称DCL)定义介绍
- 1.2 高并发环境下DCL的应用和优势
- 二、DCL存在的问题
- 2.1 DCL的代码示例
- 2.2 指令重排的定义和工作原理
- 2.3 指令重排导致DCL失效的情况分析
- 三、深入分析指令重排和DCL的问题
- 3.1 示例代码中Singleton对象创建过程的指令重排可能性
- 3.2 执行顺序的变化导致DCL无法正确工作的剖析
- 3.3 多线程环境下由于指令重排导致的数据不一致
- 四、解决方案探究
- 4.1 volatile关键字的介绍和应用
- 4.2 利用volatile关键字解决DCL问题的示例和分析
- 4.3 其他解决DCL问题的方案及其优缺点比较
- 5. 参考资料
一、引言
1.1 双重检查锁定(Double-Checked Locking,简称DCL)定义介绍
双重检查锁定(Double-Checked Locking)是一种并发设计模式,该模式减少了同步的开销,提高了执行效率。该模式通过两次检查锁定,确保被检查的代码的线程安全性。在第一次检查中,如果发现变量不满足条件,才进行加锁操作。然后在锁定的区块内再进行一次检查,如果仍不满足条件,才进行相关操作。
1.2 高并发环境下DCL的应用和优势
在高并发环境下,DCL可以显著提高性能。在使用单例模式时,如果没有并发考虑,可能每次访问单例对象时都需要获取同步锁,这会大大影响程序的执行效率。而DCL模式可以避免这个问题,它只在第一次实例化时加锁,之后的访问都不需要获取锁,这大大降低了锁的开销,提高了程序的执行效率。但要注意,由于JVM的指令重排优化,DCL在某些情况下可能会失效,需要慎重使用。
二、DCL存在的问题
2.1 DCL的代码示例
class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这段代码是典型的DCL实现单例模式的例子。在getInstance()方法中,先检查instance是否为null,如果为null,才对Singleton.class对象加锁,然后在锁定区域内再次检查instance是否为null,如果还是null,就创建一个Singleton实例。
2.2 指令重排的定义和工作原理
指令重排是为了提高处理器性能,允许编译器和处理器调整指令的执行顺序。一旦保证最终执行结果与代码顺序执行的结果一致,即使没有按照代码原有的顺序执行也不影响。
2.3 指令重排导致DCL失效的情况分析
在上述DCL代码示例中,instance = new Singleton();
这行代码实际上涉及到三个操作:
1)为Singleton分配内存空间;
2)调用Singleton的构造函数,初始化成员字段;
3)将instance对象指向分配的内存空间。
但由于JVM的指令重排优化,执行顺序可能变成1-3-2。也就是说,先为Singleton分配内存空间,然后将instance指向该内存空间,最后调用Singleton的构造函数。在多线程环境下,如果一个线程执行到3,另一个线程刚好执行到第一次检查,发现instance不为null,就直接返回instance,此时得到的Singleton实例其实是未初始化的。这就是JVM的指令重排导致DCL失效的情况。
三、深入分析指令重排和DCL的问题
3.1 示例代码中Singleton对象创建过程的指令重排可能性
在我们的示例代码中,创建Singleton对象的过程,原本的执行顺序是1-2-3,但是由于JVM优化,可能被重新排序为1-3-2。
这种指令的重排,并不是随机的,JVM采用的是"as-if-serial"语义,也就是说,在不改变单线程程序执行结果的前提下,JVM可以对指令进行重新排序。
3.2 执行顺序的变化导致DCL无法正确工作的剖析
由于JVM的指令重排优化,如果执行顺序变为1-3-2,虽然在单线程环境下程序的结果并未改变,但是在多线程环境下,可能导致DCL无法正确工作。
具体来说,当一个线程正在执行到步骤3,也就是将instance指向分配的内存空间,但是还没有执行到步骤2,即初始化Singleton对象。此时,如果另一个线程执行到第一次检查instance是否为null,由于instance已经指向了一个内存空间,所以检查结果不为null,于是直接返回instance。但此时返回的Singleton对象其实还没有被初始化,就会出现问题。
3.3 多线程环境下由于指令重排导致的数据不一致
在多线程环境下,由于指令重排,可能导致数据的不一致。因为指令重排会改变代码的执行顺序,而在多线程环境下,线程之间是并发执行的,对于共享变量的操作顺序,可能会出现预期之外的结果。
例如,在上述例子中,由于指令重排,导致Singleton对象在被一个线程使用前,其实还没有被完全初始化,这就是一个典型的由于指令重排导致的数据不一致的问题。
四、解决方案探究
4.1 volatile关键字的介绍和应用
volatile是Java提供的一种轻量级的同步机制。它有两个主要的特性:保证可见性和禁止指令重排。保证可见性指的是当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去主存中读取新值。而禁止指令重排则是通过插入内存屏障来实现的。
4.2 利用volatile关键字解决DCL问题的示例和分析
我们可以通过给instance变量添加volatile关键字来解决DCL的问题。代码如下:
public class Singleton {
private static volatile Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
在这个实例中,volatile会强制将对instance的写操作刷新到主存,这样当其他线程去读取instance的时候,将总是读取到最新的值。同时,volatile也可以防止JVM对指令进行重新排序,从而避免出现我们之前提到的问题。
4.3 其他解决DCL问题的方案及其优缺点比较
- 急切初始化:这种方式是在类加载时就马上创建实例,优点是实现简单,线程安全,但是缺点是如果这个实例很少被使用,那么这种方式就显得有些浪费资源。
- 使用静态内部类:利用了Java的类加载机制来保证初始化instance时只有一个线程,这种方式既实现了线程安全,也达到了懒加载的效果,是一种比较推荐的方式。
- 使用枚举:这是《Effective Java》作者Josh Bloch 提倡的方式,不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化,是一种更简洁、高效的方式。
5. 参考资料
- 《深入理解Java虚拟机:JVM高级特性与最佳实践》周志明。
- 《Effective Java》
- java 内存模型