MENU

Java杂记(九)多线程访问共享变量的一个小疑问

September 7, 2018 • Code

前言:在知识星球上看到一个提问:

自己动手实现并验证了下。

主要过程

这个问题真正的原因其实就是线程没有及时地将主内存中已改变的 flag 值刷新到工作内存中,所以将 flag 变为 volatile 或增加锁即可解决。

但关键在于为什么有无方法体为影响到程序的执行行为?

刚开始初步想法是循环体中无方法体时,编译器自动将 flag 「优化」成了 true,但后来又否定掉了这个想法,因为 flag 只是普通的成员变量,并不是常量或静态变量。总之先写一个 Demo,再反编译看看结果。

  • Demo
public class ThreadDemo extends Thread {

    private boolean flag = true;

    @Override
    public void run() {
        System.out.println("============ run 方法开始 ============");
        while (flag) {
        }
        System.out.println("============ run 方法结束 ============");
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadDemo t = new ThreadDemo();
        t.start();
        Thread.sleep(1000);
        t.flag = false;
        System.out.println("============ flag 修改为 false 成功 ============");
    }
}
  • 运行结果:确实陷入了死循环
  • 反编译结果:

从反编译的结果来看,确实不应该陷入死循环,就算 flag 的刷新不够及时,但最后也会刷新。

而一旦尝试在循环中加入打印语句之类的,程序就可以正常退出,确实是如问题所述的情况。

JIT 编译优化问题

在反编译中找不到结果,会不会是 JIT 优化了代码?

在编译时增加如下配置:

-server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading  -XX:+PrintAssembly -XX:+LogCompilation

得到所需要的 hotspot_pidxxx.log 文件,用 JITWatch 打开并分析,最终发现了原因:确实是 JIT 优化了代码,导致循环中 flag 一直使用了寄存器中的缓存值。

JITWatch 记得配置 config,才能显示匹配的源代码。

结论

  • 当循环中没有方法体或方法体耗时少(i++等)的时候,在 main 线程 sleep 1 秒的过程中,while 循环的循环次数超过了 JIT 阀值,所以进行了 JIT 优化,将 flag 值缓存起来了,故主线程中对 flag 值的改变无法被循环体线程获知,陷入死循环
  • 而一旦有耗时较长的方法体时,循环次数在主线程 sleep 过程中无法达到阀值,所以正常执行退出。

验证:将 main 中 sleep 语句删除,空方法体也正常退出,因循环无法达到指定次数。

Last Modified: October 29, 2018
Archives QR Code
QR Code for this page
Tipping QR Code
Leave a Comment