Java普通变量与volatile变量赋值重排序问题及解决方案咨询
嘿,这个问题抓得很准——结合JSR-133的重排序规则和你提到的“无法控制的中间代码”,我们可以一步步拆解:
1. m_normal的赋值能否被重排序到m_volatile的赋值之后?
你参考的JSR-133 Cookbook表格里的重排序规则是核心,结合你无法控制中间代码的前提,分两种情况看:
- 如果中间的
...代码没有任何volatile操作:根据规则,普通写和后续的volatile写是禁止重排序的——编译器和处理器不能把volatile写操作提前到普通写之前执行。这种情况下m_normal的赋值一定早于m_volatile的赋值。 - 如果中间的
...代码包含volatile读操作:情况就会变化!规则里明确,普通写和volatile读是允许重排序的,也就是说m_normal的赋值可能被推迟到volatile读之后;而volatile读和后续的volatile写是禁止重排序的,这就会导致m_normal的赋值最终被重排序到m_volatile的赋值之后。此时对其他线程来说,m_volatile的赋值会先可见,而m_normal的赋值还没完成。
2. get()方法是否可能返回null?
答案是完全有可能,尤其是当中间的...代码存在volatile操作时。当另一个线程调用get():
- 先读到
m_volatile不为null(说明volatile写已经对该线程可见); - 此时m_normal的赋值可能还没完成,或者还没刷新到主内存对该线程可见;
- 最终
get()返回null。
就算中间代码没有volatile操作,我们也不能依赖这个假设(毕竟你无法控制这部分代码),所以必须按存在风险的情况处理。
3. 最佳解决方案
既然无法控制...处的代码,我们需要让m_normal的赋值和m_volatile的赋值之间建立可靠的happens-before关系,不管中间代码是什么,这里有几个常用的稳妥方案:
方案一:把m_normal也声明为volatile
直接将m_normal定义为volatile变量,利用volatile的内存语义禁止重排序并保证可见性:
private volatile Object m_normal; private volatile Object m_volatile;
这种方式开销低,实现简单,volatile写的性能损耗在大多数场景下可以忽略。
方案二:使用synchronized同步块
把赋值和读取逻辑都放到同一个synchronized块中,利用锁的原子性和内存可见性保证:
public void change() { synchronized (this) { m_normal = new Object(); ... // 无法控制的代码 m_volatile = new Object(); } } public Object get() { synchronized (this) { Object v = m_volatile; if (v != null) { return m_normal; } return null; } }
synchronized会保证块内操作的原子性,释放锁时会把所有写操作刷新到主内存,其他线程获取锁时会读取最新的主内存值,完全避免重排序问题。缺点是开销比volatile略大,但如果调用频率不高,这是最稳妥的方案。
方案三:用AtomicReference包装m_normal
如果m_normal是复杂对象,可以用AtomicReference来包装,它的set()方法具有volatile写的语义:
private AtomicReference<Object> m_normal = new AtomicReference<>(); private volatile Object m_volatile; public void change() { m_normal.set(new Object()); ... // 无法控制的代码 m_volatile = new Object(); }
这种方式既保证了m_normal赋值的volatile语义,又能适配复杂对象的场景。
内容的提问来源于stack exchange,提问作者Nathan

