双重检查锁定(DCL)中volatile关键字的必要性及相关疑问
Hey there! 看得出来你已经把《Java并发编程实战》里的DCL知识点啃得很深入了,你的理解完全没错——不用volatile的话,线程确实可能拿到一个“半初始化”的对象。我猜你想问的是**“既然同步代码块已经能保证可见性,为啥还要给resource字段加volatile?”** 这绝对是并发领域里最容易踩坑的误区之一,我来给你讲透:
先复盘下无volatile的DCL到底哪里炸锅
先上一段典型的有问题的DCL代码:
public class Singleton { private static Singleton resource; // 没加volatile private Singleton() {} public static Singleton getInstance() { if (resource == null) { // 第一次检查:无锁 synchronized (Singleton.class) { if (resource == null) { // 第二次检查:加锁 resource = new Singleton(); // 这里藏着大问题! } } } return resource; } }
你可能不知道,
resource = new Singleton();这一行在JVM层面会拆成3个独立操作:
- 给Singleton对象分配内存空间
- 执行构造函数,初始化对象里的所有字段
- 把
resource变量指向刚分配的内存地址(此时resource不再是null)
在没有volatile的情况下,JVM的即时编译器会搞指令重排序——为了提速,它可能把步骤2和3调换顺序:先把resource赋值为非null的内存地址,再去初始化对象。这时候如果另一个线程刚好走到第一次检查,看到resource不是null,直接就返回这个还没初始化完的“半成品”对象,调用它的方法大概率会出各种诡异的bug(比如空指针、字段值不对)。
为啥同步代码块救不了这个场景?
你可能会疑惑:同步代码块不是能保证原子性和可见性吗?没错,但问题出在第一次检查是在同步块外面做的!
假设线程A在同步块里执行了重排序后的操作:先把resource指向内存,还没初始化对象就退出了同步块。这时候线程B走到if (resource == null),看到resource已经不是null,直接返回了——它根本没进入同步块,所以同步块的可见性保证对它完全无效,自然看不到线程A还没做完的对象初始化。
volatile在这里的核心作用
volatile关键字主要干了两件事,刚好命中DCL的痛点:
- 禁止指令重排序:JVM会保证对象初始化(步骤2)完成后,才会把resource赋值为非null引用(步骤3),彻底杜绝“半成品”对象被暴露的可能。
- 保证可见性:当一个线程修改了volatile变量,其他线程能立刻读到最新值。不过这里更关键的是前面的禁止重排序功能——毕竟如果重排序被禁止了,可见性问题其实也顺带解决了。
额外提一句:Java版本的坑
要注意,Java 5之前的volatile语义并没有明确禁止这种重排序,所以那时候哪怕加了volatile,DCL还是有问题。但Java 5重新定义了volatile的内存模型,才真正把DCL的漏洞补上,这也是《Java并发编程实战》里讨论这个问题的前提哦。
内容的提问来源于stack exchange,提问作者sdindiver

