关于instruction reordering下Holder实例发布与变量n重排及AssertionError的疑问
关于Holder实例引用发布与n赋值重排的解释
嘿,我来给你掰扯清楚这个重排到底是怎么回事儿~ 首先得先把你提到的那个经典场景明确下来,方便我们理解:
假设我们有这样的代码:
class Holder { int n; Holder() { // 给变量n赋值 n = 42; } } // 一个被多线程共享的Holder引用 Holder sharedHolder; // Thread1的执行逻辑 void thread1() { // 创建Holder实例,并把引用赋值给共享变量(这就是所谓的「引用的发布」) sharedHolder = new Holder(); } // Thread2的执行逻辑 void thread2() { Holder h = sharedHolder; if (h != null) { // 断言n的值是42 assert h.n == 42; } }
为什么会发生重排?
我们先拆解new Holder()这个操作在底层的三个核心步骤:
- 为Holder对象分配一块内存空间
- 执行Holder的构造函数,完成变量
n的赋值(也就是把42写入n的内存地址) - 将Holder对象的内存地址赋值给
sharedHolder变量(这一步就是「引用发布」)
在没有同步约束的情况下,JVM和CPU为了提升执行效率,会允许步骤2和步骤3的顺序被调换——也就是先把Holder的引用赋值给sharedHolder,再去执行构造函数给n赋值。这就是回答里说的「引用的发布与变量n的赋值发生重排」。
为什么这种重排是被允许的?
因为这两个操作(构造函数里给n赋值、把Holder引用赋值给共享变量)之间没有happens-before关系:
- Java内存模型(JMM)里的happens-before规则,只有当两个操作存在明确的约束(比如用volatile修饰sharedHolder、用synchronized同步代码块、或者你提到的thread.start()这种规则)时,才会禁止重排。
- 在这个场景里,Thread1内部的这两个操作属于「没有依赖关系的独立操作」——对JVM来说,先赋值引用还是先给n赋值,从Thread1自己的视角看结果都是一样的(Thread1后续如果访问sharedHolder.n,总能看到42),所以它会认为这种重排是安全的,可以做优化。
再补充你提到的thread.start()的情况
你说知道thread.start()有happens-before关系,这里要区分场景:如果Thread1是先创建Holder实例、给n赋值,然后调用Thread2.start(),那Thread1在start()之前的所有操作确实happens-before Thread2的所有操作,Thread2肯定能看到正确的n值。但如果是Thread1把Holder引用发布到共享变量,Thread2是独立于Thread1启动的(比如两个线程分别启动,没有通过start()建立顺序),那这种重排就可能导致Thread2拿到了非null的Holder引用,但n还没被赋值,从而触发AssertionError。
内容的提问来源于stack exchange,提问作者sdindiver
相关产品推荐
相关产品推荐

