You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何无volatile的双重检查锁定对32位原生类型有效?局部变量作用解析

32位原生类型双重检查锁定有效性的拆解

前置说明

我仅出于学术兴趣研究双重检查锁定(DCL),不会在实际生产代码中使用。我已阅读知名文章《“双重检查锁定已失效”声明》。

问题背景回顾

先看两个对照代码:

普通对象的同步安全实现

class Foo {
    private Helper helper = null;
    public synchronized Helper getHelper() {
        if (helper == null)
            helper = new Helper();
        return helper;
    }
    // 其他函数和成员...
}

普通对象DCL失效的核心是:helper = new Helper()的指令可能被重排序——分配内存、初始化对象、赋值给引用这三步里,后两步可能颠倒,导致其他线程看到非null但未初始化的helper。

32位原生类型的DCL实现

class Foo {
    private int cachedHashCode = 0;
    public int hashCode() {
        int h = cachedHashCode;
        if (h == 0)
            synchronized(this) {
                if (cachedHashCode != 0)
                    return cachedHashCode;
                h = computeHashCode();
                cachedHashCode = h;
            }
        return h;
    }
    // 其他函数和成员...
}

接下来咱们拆解两个核心问题:


1. 为啥这个32位原生类型的DCL写法是有效的?

核心要从Java内存模型规则和DCL失效本质对比来看:

  • 普通对象DCL失效的根源是对象创建的多步指令重排序,以及引用赋值和对象初始化的可见性不一致。但32位原生类型(比如int)的赋值是单一原子操作——Java规范明确规定,32位原生类型的读写不会被拆分成多个步骤,其他线程要么看到旧的完整值(这里是0),要么看到新的完整值(计算后的hashCode),绝对不会看到“半写”的中间状态。
  • 同时,32位原生类型的赋值没有对象创建时的多步拆分,也就不存在“赋值先于初始化”的重排序可能。同步块内的赋值操作完成后,其他线程读取到的一定是完整的、正确的值。

再配合双重检查的逻辑:

  • 第一次非同步检查if (h == 0):如果已经有线程完成了赋值,当前线程直接拿值返回,不用进同步块,兼顾了性能。
  • 同步块内的二次检查:防止多个线程同时通过第一次检查后重复计算hashCode,保证只有一个线程执行computeHashCode()并赋值。

因为32位原生类型的读写原子性+无重排序风险,所以这个写法是安全有效的。


2. 这里使用局部变量h的原因是什么?

这个细节藏着两个实用的设计考量:

  • 性能优化:如果直接多次读取cachedHashCode字段(第一次检查、同步块内检查、返回),每次都要访问堆内存中的字段;而局部变量存在当前线程的栈上,读取速度更快,减少了堆内存访问的开销。
  • 逻辑一致性保障:虽然32位赋值是原子的,但Java内存模型允许字段读取的重排序(比如重复读取字段时,可能拿到旧值)。把cachedHashCode先读到局部变量h后,后续的判断和返回都基于这个栈上的变量,保证了整个方法内对“当前hashCode值”的认知是一致的——不会出现刚读完cachedHashCode为0,进入同步块前又被其他线程修改,导致逻辑混乱的情况(同步块内的二次检查已经兜底,但局部变量让逻辑更严谨)。

简单说,局部变量既省性能,又让代码逻辑更稳定。


内容的提问来源于stack exchange,提问作者gstackoverflow

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:57:50