You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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的每个线程都有自己的栈帧,栈里的局部变量(包括对共享变量的副本)不会自动同步到主存,没有同步操作的话,单核心下线程切换后也可能看不到其他线程的修改。

3. 内存可见性问题是否与硬件架构相关?

  • 是的,但它本质上是由Java内存模型(JMM)的规则决定的,硬件只是影响问题的表现形式:
    • 不同的硬件架构(比如多核心的缓存一致性协议,如MESI)会影响缓存同步的效率,但JMM是一个抽象的模型,它定义了线程间共享变量的可见性规则——只要代码不符合JMM的规则(比如没有建立happens-before关系),不管底层硬件是什么,都可能出现可见性问题。
    • 比如即使硬件有完美的缓存一致性协议,编译器的重排序也可能绕过这些机制,导致线程看不到最新的变量值。

最后总结一下

内存可见性的核心是JMM的happens-before关系:只有当一个写操作happens-before另一个读操作时,读操作才能保证看到写操作的结果。在你的例子里,main线程对readynumber的写操作,和ReaderThread的读操作之间没有任何happens-before关系,所以才会出现可见性问题。

解决办法也很简单:要么给ready加上volatile修饰(同时因为volatile禁止重排序,也能保证number的写操作在ready的写操作之前执行),要么用synchronized包裹读写操作,这样就能建立合法的happens-before关系,保证可见性。

内容的提问来源于stack exchange,提问作者Thread.start

火山引擎 最新活动