双重检查锁定(Double-Checked Locking)是一种并发设计模式,该模式减少了同步的开销,提高了执行效率。该模式通过两次检查锁定,确保被检查的代码的线程安全性。在第一次检查中,如果发现变量不满足条件,才进行加锁操作。然后在锁定的区块内再进行一次检查,如果仍不满足条件,才进行相关操作。
在高并发环境下,DCL可以显著提高性能。在使用单例模式时,如果没有并发考虑,可能每次访问单例对象时都需要获取同步锁,这会大大影响程序的执行效率。而DCL模式可以避免这个问题,它只在第一次实例化时加锁,之后的访问都不需要获取锁,这大大降低了锁的开销,提高了程序的执行效率。但要注意,由于JVM的指令重排优化,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实例。
指令重排是为了提高处理器性能,允许编译器和处理器调整指令的执行顺序。一旦保证最终执行结果与代码顺序执行的结果一致,即使没有按照代码原有的顺序执行也不影响。
在上述DCL代码示例中,instance = new Singleton();
这行代码实际上涉及到三个操作:
1)为Singleton分配内存空间;
2)调用Singleton的构造函数,初始化成员字段;
3)将instance对象指向分配的内存空间。
但由于JVM的指令重排优化,执行顺序可能变成1-3-2。也就是说,先为Singleton分配内存空间,然后将instance指向该内存空间,最后调用Singleton的构造函数。在多线程环境下,如果一个线程执行到3,另一个线程刚好执行到第一次检查,发现instance不为null,就直接返回instance,此时得到的Singleton实例其实是未初始化的。这就是JVM的指令重排导致DCL失效的情况。
在我们的示例代码中,创建Singleton对象的过程,原本的执行顺序是1-2-3,但是由于JVM优化,可能被重新排序为1-3-2。
这种指令的重排,并不是随机的,JVM采用的是"as-if-serial"语义,也就是说,在不改变单线程程序执行结果的前提下,JVM可以对指令进行重新排序。
由于JVM的指令重排优化,如果执行顺序变为1-3-2,虽然在单线程环境下程序的结果并未改变,但是在多线程环境下,可能导致DCL无法正确工作。
具体来说,当一个线程正在执行到步骤3,也就是将instance指向分配的内存空间,但是还没有执行到步骤2,即初始化Singleton对象。此时,如果另一个线程执行到第一次检查instance是否为null,由于instance已经指向了一个内存空间,所以检查结果不为null,于是直接返回instance。但此时返回的Singleton对象其实还没有被初始化,就会出现问题。
在多线程环境下,由于指令重排,可能导致数据的不一致。因为指令重排会改变代码的执行顺序,而在多线程环境下,线程之间是并发执行的,对于共享变量的操作顺序,可能会出现预期之外的结果。
例如,在上述例子中,由于指令重排,导致Singleton对象在被一个线程使用前,其实还没有被完全初始化,这就是一个典型的由于指令重排导致的数据不一致的问题。
volatile是Java提供的一种轻量级的同步机制。它有两个主要的特性:保证可见性和禁止指令重排。保证可见性指的是当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去主存中读取新值。而禁止指令重排则是通过插入内存屏障来实现的。
我们可以通过给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对指令进行重新排序,从而避免出现我们之前提到的问题。