Java双重检查锁定模式中tempWrapper为何是正确性必需的?
为什么双重检查锁定中使用FinalWrapper必须用局部变量tempWrapper?
先看两种实现的对比,先放正确的带局部变量的代码:
public class DoubleCheckedLocking { private static FinalWrapper<Helper> helperWrapper; public static Helper getHelper() { // 必须的局部变量 FinalWrapper<Helper> tempWrapper = helperWrapper; if (tempWrapper == null) { synchronized (DoubleCheckedLocking.class) { if (helperWrapper == null) { helperWrapper = new FinalWrapper<>(new Helper()); } // 同步块内更新局部变量 tempWrapper = helperWrapper; } } return tempWrapper.value; } private static class FinalWrapper<T> { public final T value; public FinalWrapper(T value) { this.value = value; } } }
如果直接去掉局部变量,改成下面的代码,会存在潜在问题:
// 错误实现,可能失效 public static Helper getHelper() { if (helperWrapper == null) { synchronized (DoubleCheckedLocking.class) { if (helperWrapper == null) { helperWrapper = new FinalWrapper<>(new Helper()); } } } return helperWrapper.value; }
核心原因:Java内存模型允许的读重排序
Java内存模型(JMM)不会强制约束线程对共享变量的多次读取顺序,甚至允许把helperWrapper.value的读取操作拆解后重排序。具体来说:
读取helperWrapper.value会被拆成两步:
- 读取共享变量
helperWrapper的引用到线程寄存器 - 读取该引用指向对象的
value字段
JMM允许把**第一步(读helperWrapper引用)**提前到if (helperWrapper == null)的检查之前执行。这就会引发问题:
- 线程B先执行第一步,读到
helperWrapper的旧值(null),暂存在寄存器里 - 此时线程A完成了
helperWrapper的初始化,把非null的引用写入主内存 - 线程B再执行
if (helperWrapper == null)检查,读到主内存里的非null值,跳过同步块 - 最后线程B用寄存器里缓存的旧null引用去读
value字段,直接抛出NullPointerException
局部变量tempWrapper的作用
用局部变量后,所有操作都基于线程私有的局部变量,彻底避免了共享变量读取的重排序问题:
- 第一次读取共享变量
helperWrapper到局部变量tempWrapper,这是线程私有的,JMM不会对局部变量的操作做重排序 - 后续的null检查和value读取都基于同一个局部变量,要么是null(进入同步块完成初始化),要么是同步块内更新后的正确引用
- 同步块内重新赋值
tempWrapper = helperWrapper,保证拿到的是最新初始化后的FinalWrapper引用,其value字段因final语义已经安全发布,不会出现未初始化的情况
你之前的理解偏差
你提到"若其他线程读到helperWrapper不为null,那么value的值必然是正确的"——这个结论本身是对的,但问题出在你无法保证读取helperWrapper.value时,用的是和之前null检查时同一个helperWrapper引用。JMM的读重排序可能让你在检查时读到非null,但读value时用的是更早读到的null引用,这才是风险点。
内容的提问来源于stack exchange,提问作者lol
相关产品推荐
相关产品推荐

