Java 多线程共享变量可见性研究

94 阅读3分钟

测试代码

Thread thread1 = new Thread(new Runnable() {
    int i = 0;
    @Override
    public void run() {
        while (!flag){
            i++;
        }
    }
});
thread1.start();
flag = true;

上述代码是一直循环还是set flag = true之后立即结束?

答案是立即结束。

根据Java并行开发的经典理论,thread1会在其工作内存中保存一份flag值的副本,因此当主线程设置flag为true时,在未使用volatile修饰此变量的情况下,thread1的工作内存中flag暂时应该还是false,不会立即终止循环。然而在IDEA IDE中运行上述代码后,程序会立即结束,理论自然是正确的,为何会得出相悖的结果?可能有如下原因:

  • 主线程运行结束后整个程序会立即终止,但是当去掉最后一行代码后,程序还是在运行中,说明陷入了死循环,也证实了并非主线程运行结束了,所以整个程序才结束了。

  • 主线程结束后,将对flag的修改从其工作内存中刷新回了主内存。无法验证,何时把共享变量的值刷回主内存是一件不确定的事情。

  • 可能是JIT做了什么处理,让thread1每次读取flag时都从主内存中读取,看起来仿佛给此变量使用volatile修饰一样。不过笔者在使用Jitwatch+HSDIS在jdk17,MacOS平台下并未能看到相应的汇编代码,只能暂时作罢,等后面有空了再研究一下。

  • 最后一种可能最简单,在thread1的工作线程在从主内存读取flag变量值到工作内存之前,flag就已经被设置成true了,想验证也很简单,在10和11行代码之间加一个Thread.sleep(1000)即可,如下所示:

    Thread thread1 = new Thread(new Runnable() {
    int i = 0;
    @Override
    public void run() {
        while (!flag){
            i++;
        }
    }
    });
    thread1.start();
    Thread.sleep(1000);
    flag = true;
    

    上述代码会无限循环,看起来永远无法获取到flag的最新值。问题就出来了,即使不使用volatile修饰,现代计算机应该会在一个较短的时间内将修改值刷新回主内存,但现在看起来并不会,具体原因可以参考你了解的可见性可能是错的这篇文章,是JIT将代码“优化”成了一个无限循环代码。再稍微延伸一下,如何用最简单的方式让thread1使用flag的最新值,只需要加一个print语句即可。

     Thread thread1 = new Thread(new Runnable() {
     int i = 0;
     @Override
     public void run() {
         while (!flag){
             System.out.println();
         }
     }
     });
     thread1.start();
     Thread.sleep(1000);
     flag = true;
    

    thread1在运行一段时间后就会终止。

    截屏2025-06-29 15.25.41.png 上面的一大段空白,都是print输出空白行所致,最后运行结束。这是因为print内部使用了syncronized关键字,强制工作内存变量副本失效,重新从主内存读取最新值。

    public void println() {
        newLine();
    }
    
    public void println(boolean x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }