Java双重检查锁为什么使用volatile关键字?(新手必看)
双重检查锁(Double-Checked Locking,DCL)以及 volatile 关键字的使用是并发编程中一个非常经典的问题。
在多线程编程中,单例模式的线程安全实现常常会使用到 DCL 模式。这种模式在一定程度上减少了同步的开销,但如果没有正确使用,它会导致复杂的并发问题。Java 语言提供了 volatile 关键字来方便地实现正确的 DCL。
单例模式意味着某个类的实例在整个应用程序中只有一个。在多线程环境下,我们必须确保单例类的对象在多个线程中只被创建一次。最简单的线程安全单例模式实现是对整个创建方法加锁,但这会引入不必要的同步开销,因为一旦实例被创建,就不再需要同步控制了。
为了减少同步带来的开销,DCL 利用两次检查和一次锁定来实现线程安全的单例。第一次检查是为了避免不必要的同步,第二次检查是为了在 null 实例的情况下进行同步创建。
DCL 的示例代码如下:
我们知道,volatile 关键字主要有两个作用:
上述示例代码中,一个线程执行过程如下:
上述过程看起来是 3 个步骤,实际上包含很多指令,instance = new Singleton();对象创建过程就可以分解为下面步骤:
如果我们不使用 volatile 关键字修饰,可能会导致这些指令的执行顺序被重排序,例如,指令 3 可能在指令 2 之前执行。这意味着在调用构造函数初始化对象之前,instance 的值已经不是 null 了。如果此时,线程 B 开始了 getInstance() 方法的第一次检查,并且看到 instance 的值不是 null,那么线程 B 将返回一个尚未完全构造的对象。
因此 volatile 关键字是 DCL 的重要部分,它解决了指令重排序问题和变量可见性问题。这确保了实例化操作的安全性,允许我们高效地实现线程安全的单例模式。
在多线程编程中,单例模式的线程安全实现常常会使用到 DCL 模式。这种模式在一定程度上减少了同步的开销,但如果没有正确使用,它会导致复杂的并发问题。Java 语言提供了 volatile 关键字来方便地实现正确的 DCL。
单例模式意味着某个类的实例在整个应用程序中只有一个。在多线程环境下,我们必须确保单例类的对象在多个线程中只被创建一次。最简单的线程安全单例模式实现是对整个创建方法加锁,但这会引入不必要的同步开销,因为一旦实例被创建,就不再需要同步控制了。
为了减少同步带来的开销,DCL 利用两次检查和一次锁定来实现线程安全的单例。第一次检查是为了避免不必要的同步,第二次检查是为了在 null 实例的情况下进行同步创建。
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; } }在上述代码中,instance 使用 volatile 修饰,这是实现 DCL 的关键。很多读者可能会产生疑问,为什么要使用 volatile 关键字呢?它起到了什么作用?
我们知道,volatile 关键字主要有两个作用:
- 确保变量的可见性:保证一个线程写入的变量值对其他线程立即可见。
- 防止指令重排序:在运行时,编译器和处理器可能会对指令进行重排序,以优化性能和利用资源。volatile 关键字可以防止创建对象时的指令重排序。
上述示例代码中,一个线程执行过程如下:
- 线程 A 进入 getInstance() 方法;
- 因为 instance 为 null,线程 A 进入同步块;
- 在同步块内,线程 A 执行 instance = new Singleton()。
上述过程看起来是 3 个步骤,实际上包含很多指令,instance = new Singleton();对象创建过程就可以分解为下面步骤:
- 给 Singleton 实例分配内存空间;
- 调用 Singleton 的构造函数,初始化成员字段;
- 将 instance 指向分配的内存空间(此时 instance 的值就不是 null 了)。
如果我们不使用 volatile 关键字修饰,可能会导致这些指令的执行顺序被重排序,例如,指令 3 可能在指令 2 之前执行。这意味着在调用构造函数初始化对象之前,instance 的值已经不是 null 了。如果此时,线程 B 开始了 getInstance() 方法的第一次检查,并且看到 instance 的值不是 null,那么线程 B 将返回一个尚未完全构造的对象。
因此 volatile 关键字是 DCL 的重要部分,它解决了指令重排序问题和变量可见性问题。这确保了实例化操作的安全性,允许我们高效地实现线程安全的单例模式。