为何Java多线程下long类型撕裂现象无法复现?求解答
关于Java long类型写入撕裂现象的复现问题
嘿,这个问题我碰到过好多次——不少开发者都知道JLS里提到long/double的非原子性写入,但真动手复现撕裂现象时却屡屡失败,完全不是你的问题,我来帮你拆解清楚~
先给你吃个定心丸:你没误解规范
JLS 17.7确实明确规定了:对于64位的long和double类型,Java虚拟机允许将单个写入操作拆分为两个独立的32位写入操作(分别处理高32位和低32位)。在多线程场景下,如果一个线程正在写入这两个32位部分,另一个线程恰好读取,就可能得到一个混合值——也就是一次写入的前半段和另一次写入的后半段组合出来的“撕裂值”,比如原本交替写入0x0000000000000000和0xFFFFFFFFFFFFFFFF,结果读到0x00000000FFFFFFFF这种中间态。
为什么复现这么难?
问题出在硬件和JVM实现的实际行为上,理论上的可能性被现实条件限制了:
- 硬件原子性支持:现在主流的64位CPU(x86_64、ARM64等)本身对64位内存访问是原子性的,哪怕JVM允许拆分,实际执行时也会直接用硬件提供的原子操作,自然不会出现撕裂。只有在32位CPU或者不支持64位原子访问的老旧硬件上,才会触发JVM的拆分逻辑。
- JVM优化:即使在理论上可以拆分的环境中,JVM也可能对非volatile的long/double写入做优化,直接用原子操作完成,进一步降低了撕裂的概率。另外,如果你的代码里给long变量加了
volatile修饰,那JLS要求这种写入必须是原子性的,肯定复现不了撕裂。 - 极小的时间窗口:哪怕所有条件都满足,要刚好卡到“写入线程刚写完32位,读取线程就执行读取”的时机,概率极低——就像你要在高速行驶的汽车上接住掉下来的硬币一样,不是不可能,但很难碰到。
怎么提高复现概率?
如果一定要复现真实的JVM撕裂行为,可以试试这些方法:
- 切换到32位JVM:32位JVM无法直接执行64位原子写入,必须拆成两个32位操作,这是最容易复现的场景。
- 增加线程压力:让读写线程疯狂循环执行,比如写入线程交替写入两个极端值,读取线程持续检查是否出现非预期值,通过大量的循环次数来提高碰到时间窗口的概率。
- 模拟撕裂行为:如果找不到32位环境,可以手动把long拆成两个int变量分别写入,模拟JVM的拆分逻辑,这样能快速看到撕裂效果(虽然这是模拟,不是真实JVM行为,但能验证你对撕裂现象的理解)。
一个参考的复现代码示例
下面是一段可能复现撕裂的代码,建议在32位JVM上运行:
public class LongTearDemo { // 注意:不要加volatile,否则写入会变成原子操作 private static long sharedValue = 0L; public static void main(String[] args) throws InterruptedException { // 写入线程:交替写入全0和全1的long值 Thread writer = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { sharedValue = 0x0000000000000000L; sharedValue = 0xFFFFFFFFFFFFFFFFL; } }); // 读取线程:检查是否出现既不是全0也不是全1的撕裂值 Thread reader = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { long current = sharedValue; if (current != 0x0000000000000000L && current != 0xFFFFFFFFFFFFFFFFL) { System.out.println("捕获到撕裂值:0x" + Long.toHexString(current)); // 找到结果后终止所有线程 writer.interrupt(); Thread.currentThread().interrupt(); } } }); writer.start(); reader.start(); writer.join(); reader.join(); } }
在32位JVM上运行这段代码,大概率几分钟内就能捕获到撕裂值;但在64位JVM上,可能跑几个小时都出不来结果——这就是硬件原子性带来的差异。
内容的提问来源于stack exchange,提问作者kasterma
相关产品推荐
相关产品推荐

