经典的面试题~DCL需不需要被volatile关键字修饰?为什么?
欢迎大家搜索“小猴子的技术笔记”关注我的公众号,有问题可以及时和我交流。
DCL(Double-Checked Locking)双重检查锁。在Java的多线程中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销,使用双重检查所是常见的延迟初始化的技术。但是,要正确使用线程安全的延迟初始化需要一些技巧,否则很容易出现问题。
首先来看看下面这段代码。下面的代码是一个典型的懒加载单例模式的实现,使用了延迟加载来降低同步的开销。请你猜一猜它会不会有线程安全的问题:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
// 第一次检查instance是否为空
if (instance == null) {
// 加锁进行instance的初始化
synchronized (Singleton.class) {
// 再次判断instance是否为空
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
也许从代码来看似乎很完美。因为如果第一次检查instance不为null,就不会继续执行加锁的方法,而是直接返回实例化好的instance。这样来看确实大幅降低了synchronized带来的性能开销。如果为null,就会继续执行加锁的方法。如果一个线程拿到锁了,就会再次检查instance是否为null,如果不为null就进行实例化。
也许它就是你看到的表面完美,因为它是存在错误的。如果执行到第一次检查instance不为null的时候,instance引用的对象可能还没有完成初始化。
上述代码“instance = new Singleton()”的目的是创建一个对象,这一行代码的从无到有可以经历如下的步骤:
注意:在一些JIT编译器上,步骤2和步骤3是有可能发生重排序的。重排序之后的执行顺序如下:
因此我们回到上面的代码示例中:假设这个单例还没有被初始化,它也允许了JIT编译器对步骤2和步骤3进行了重排序。这个时候有两个线程同时来访问这个单例,那么就有可能存在其中某一个线程访问到的对象是没有初始化的对象。
下面来做一个解析:再假设线程A先访问这个单例,然后走到重排序之后的步骤2的时候,线程B也来访问这个单例了。这个时候看到了这个单例已经在内存中存在了(因为判断对象是否为空,判断的是有没有在内存开辟空间),就会认为之前已经有人已经初始化过这个单例了,就会直接返回。但是它拿到的其实是一个还没有经过初始化的单例对象。
因此上面这个示例并不是安全的单例懒加载。
如果你看过我之前写的文章介绍禁止重排序的关键字的话,你就应该能够想到volatile。因此上面代码只需要做一点点小小的修改就可以实现线程安全的延迟初始化:
public class Singleton {
// 这里增加了volatile关键字:禁止指令重排序
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;
}
}
在“private static volatile Singleton instance;”添加一个volatile关键字,可以有效禁止指令的重排序来解决DCL引发的多线程同一时间进行的第一次访问带来的问题。
请注意:我这里说的是“第一次”,因为instance被static关键字修饰了,所以它一旦初始化了就是全局的,因此这里多线程问题,也是第一次并发访问的时候出现的问题。
欢迎大家搜索“小猴子的技术笔记”关注我的公众号,有问题可以及时和我交流。
转载自:https://juejin.cn/post/6905202367185027085