为何无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
相关产品推荐
相关产品推荐

