一个 println 竟然比 volatile 还好使?

宁睿  金牌会员 | 2023-10-3 08:19:01 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 959|帖子 959|积分 2887

前两天一个小伙伴突然找我求助,说准备换个坑,最近在系统复习多线程知识,但遇到了一个刷新认知的问题……
小伙伴:Effective JAVA 里的并发章节里,有一段关于可见性的描述。下面这段代码会出现死循环,这个我能理解,JMM 内存模型嘛,JMM 不保证 stopRequested 的修改能被及时的观测到。
  1. static boolean stopRequested = false;
  2. public static void main(String[] args) throws InterruptedException {
  3.     Thread backgroundThread = new Thread(() -> {
  4.         int i = 0;
  5.         while (!stopRequested) {
  6.             i++;
  7.         }
  8.     }) ;
  9.     backgroundThread.start();
  10.     TimeUnit.MICROSECONDS.sleep(10);
  11.     stopRequested = true ;
  12. }
复制代码
但奇怪的是在我加了一行打印之后,就不会出现死循环了!难道我一行 println 能比 volatile 还好使啊?这俩也没关系啊
  1. static boolean stopRequested = false;
  2. public static void main(String[] args) throws InterruptedException {
  3.     Thread backgroundThread = new Thread(() -> {
  4.         int i = 0;
  5.         while (!stopRequested) {
  6.             
  7.             // 加上一行打印,循环就能退出了!
  8.                 System.out.println(i++);
  9.         }
  10.     }) ;
  11.     backgroundThread.start();
  12.     TimeUnit.MICROSECONDS.sleep(10);
  13.     stopRequested = true ;
  14. }
复制代码
我:小伙子八股文背的挺熟啊,JMM 张口就来。

我:这个……其实是 JIT 干的好事,导致你的循环无法退出。JMM 只是一个逻辑上的内存模型规范,JIT可以根据JMM的规范来进行优化。
比如你第一个例子里,你用-Xint禁用 JIT,就可以退出死循环了,不信你试试?
小伙伴:WK,真的可以,加上 -Xint 循环就退出了,好神奇!JIT 是个啥啊?还能有这种功效?

JIT(Just-in-Time) 的优化

众所周知,JAVA 为了实现跨平台,增加了一层 JVM,不同平台的 JVM 负责解释执行字节码文件。虽然有一层解释会影响效率,但好处是跨平台,字节码文件是平台无关的。

在 JAVA 1.2 之后,增加了即时编译(Just-in-Time Compilation,简称 JIT)的机制,在运行时可以将执行次数较多的热点代码编译为机器码,这样就不需要 JVM 再解释一遍了,可以直接执行,增加运行效率。


但 JIT 编译器在编译字节码时,可不仅仅是简单的直接将字节码翻译成机器码,它在编译的同时还会做很多优化,比如循环展开、方法内联等等……

这个问题出现的原因,就是因为 JIT 编译器的优化技术之一 -表达式提升(expression hoisting)导致的。
表达式提升(expression hoisting)

先来看个例子,在这个hoisting方法中,for 循环里每次都会定义一个变量y,然后通过将 x*y 的结果存储在一个 result 变量中,然后使用这个变量进行各种操作
  1. public void hoisting(int x) {
  2.         for (int i = 0; i < 1000; i = i + 1) {
  3.                 // 循环不变的计算
  4.                 int y = 654;
  5.                 int result = x * y;
  6.                
  7.                 // ...... 基于这个 result 变量的各种操作
  8.         }
  9. }
复制代码
但是这个例子里,result 的结果是固定的,并不会跟着循环而更新。所以完全可以将 result 的计算提取到循环之外,这样就不用每次计算了。JIT 分析后会对这段代码进行优化,进行表达式提升的操作:
  1. public void hoisting(int x) {
  2.         int y = 654;
  3.         int result = x * y;
  4.    
  5.         for (int i = 0; i < 1000; i = i + 1) {       
  6.                 // ...... 基于这个 result 变量的各种操作
  7.         }
  8. }
复制代码
这样一来,result 不用每次计算了,而且也完全不影响执行结果,大大提升了执行效率。
注意,编译器更喜欢局部变量,而不是静态变量或者成员变量;因为静态变量是“逃逸在外的”,多个线程都可以访问到,而局部变量是线程私有的,不会被其他线程访问和修改。

编译器在处理静态变量/成员变量时,会比较保守,不会轻易优化。

像你问题里的这个例子中,stopRequested就是个静态变量,编译器本不应该对其进行优化处理;
  1. static boolean stopRequested = false;// 静态变量
  2. public static void main(String[] args) throws InterruptedException {
  3.     Thread backgroundThread = new Thread(() -> {
  4.         int i = 0;
  5.         while (!stopRequested) {
  6.                         // leaf method
  7.             i++;
  8.         }
  9.     }) ;
  10.     backgroundThread.start();
  11.     TimeUnit.MICROSECONDS.sleep(10);
  12.     stopRequested = true ;
  13. }
复制代码
但由于你这个循环是个leaf method,即没有调用任何方法,所以在循环之中不会有其他线程会观测到stopRequested值的变化。那么编译器就冒进的进行了表达式提升的操作,将stopRequested提升到表达式之外,作为循环不变量(loop invariant)处理:
  1. int i = 0;
  2. boolean hoistedStopRequested = stopRequested;// 将stopRequested 提升为局部变量
  3. while (!hoistedStopRequested) {   
  4.         i++;
  5. }
复制代码
这样一来,最后将stopRequested赋值为 true 的操作,影响不了提升的hoistedStopRequested的值,自然就无法影响循环的执行了,最终导致无法退出。

至于你增加了println之后,循环就可以退出的问题。是因为你这行 println 代码影响了编译器的优化。println 方法由于最终会调用 FileOutputStream.writeBytes 这个 native 方法,所以无法被内联优化(inling)。而未被内敛的方法调用从编译器的角度看是一个“full memory kill”,也就是说副作用不明、必须对内存的读写操作做保守处理

在这个例子里,下一轮循环的stopRequested读取操作按顺序要发生在上一轮循环的 println 之后。这里“保守处理”为:就算上一轮我已经读取了stopRequested的值,由于经过了一个副作用不明的地方,再到下一次访问就必须重新读取了。

所以在你增加了 prinltln 之后,JIT 由于要保守处理,重新读取,自然就不能做上面的表达式提升优化了。

以上对表达式提升的解释,总结摘抄自R大知乎回答。R大,行走的 JVM Wiki!

我:“这下明白了吧,这都是 JIT 干的好事,你要是禁用 JIT 就没这问题了”
<blockquote>
小伙伴:“WK
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

宁睿

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表