Java多线程内存可见性问题答疑——基于《Java并发编程实战》示例
关于Java内存可见性的核心问题解答
嘿,这个问题问得特别到位——很多人刚啃《Java并发编程实战》的时候,都会在内存可见性这块卡壳,尤其是对缓存同步、指令重排序这些底层细节搞不清。我来结合你提到的NoVisibility例子,一步步给你拆解清楚。
先把你贴的代码再放一遍方便对照:
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while(!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args){ new ReaderThread().start(); number=42; ready=true; } }
首先说为什么Thread.yield()救不了这个无限循环
Thread.yield()的作用只是让当前线程主动让出CPU时间片,给其他线程运行的机会,但它没有任何内存同步的语义——也就是说,它既不会强制当前线程把缓存里的变量刷新到主存,也不会强制线程去主存重新读取最新的变量值。Java内存模型(JMM)里只有特定操作(比如volatile修饰、synchronized代码块/方法、Lock接口的操作等)才会触发内存屏障,保证缓存和主存的同步。所以ReaderThread每次调用yield后,还是可能继续用自己缓存里的ready旧值,继续死循环。
你的几个具体问题解答
1. CPU缓存中的值多久会刷新/同步到主存?是否存在永远不同步的可能?
- 没有固定的刷新时间!缓存的刷新时机完全由硬件(比如缓存行淘汰策略、缓存一致性协议)和JVM的实现决定,可能是缓存行满了、发生缓存失效、或者遇到内存屏障指令的时候才会触发刷新。
- 确实存在永远不同步的可能!最常见的原因是编译器/CPU的指令重排序和缓存优化:
比如JIT编译器可能会把ReaderThread的循环优化成这样:
编译器觉得没有同步机制的情况下,boolean localReady = ready; // 把ready缓存到线程本地的寄存器/缓存 while(!localReady) Thread.yield();ready的值不会被其他线程修改,所以直接把它缓存起来,永远不重新读取主存的ready值,这就导致无限循环。
2. 单个CPU核心+单个CPU缓存的情况下,也会出现内存可见性问题吗?
- 会的!很多人误以为单核心就没有可见性问题,其实大错特错。因为即使是单核心,以下两种情况还是会导致可见性问题:
- 编译器/CPU的指令重排序:比如main线程里的
number=42; ready=true;可能被重排成ready=true; number=42;,这样ReaderThread可能看到ready为true,但number还是初始值0; - 线程栈的缓存:JVM的每个线程都有自己的栈帧,栈里的局部变量(包括对共享变量的副本)不会自动同步到主存,没有同步操作的话,单核心下线程切换后也可能看不到其他线程的修改。
- 编译器/CPU的指令重排序:比如main线程里的
3. 内存可见性问题是否与硬件架构相关?
- 是的,但它本质上是由Java内存模型(JMM)的规则决定的,硬件只是影响问题的表现形式:
- 不同的硬件架构(比如多核心的缓存一致性协议,如MESI)会影响缓存同步的效率,但JMM是一个抽象的模型,它定义了线程间共享变量的可见性规则——只要代码不符合JMM的规则(比如没有建立
happens-before关系),不管底层硬件是什么,都可能出现可见性问题。 - 比如即使硬件有完美的缓存一致性协议,编译器的重排序也可能绕过这些机制,导致线程看不到最新的变量值。
- 不同的硬件架构(比如多核心的缓存一致性协议,如MESI)会影响缓存同步的效率,但JMM是一个抽象的模型,它定义了线程间共享变量的可见性规则——只要代码不符合JMM的规则(比如没有建立
最后总结一下
内存可见性的核心是JMM的happens-before关系:只有当一个写操作happens-before另一个读操作时,读操作才能保证看到写操作的结果。在你的例子里,main线程对ready和number的写操作,和ReaderThread的读操作之间没有任何happens-before关系,所以才会出现可见性问题。
解决办法也很简单:要么给ready加上volatile修饰(同时因为volatile禁止重排序,也能保证number的写操作在ready的写操作之前执行),要么用synchronized包裹读写操作,这样就能建立合法的happens-before关系,保证可见性。
内容的提问来源于stack exchange,提问作者Thread.start




