一个困扰我122天的技术问题,我好像知道答案了。 (2)

简介: 一个困扰我122天的技术问题,我好像知道答案了。 (2)

第四次程序改造


再看最后一次的改造,也是致命一击的改造:


这次的改动点还是在第 9 行,把变量 i 从 基本类型 int 变成了包装类型 Integer。

来,你再猜一下...


算了,别猜了,直接喊吧:


这个程序也会正常结束。


上面的四种情况,你来品一品,你怎么解释。


Effective Java


其实在《Effective Java》这本 Java 圣典里面也提到过一嘴这个问题。

在第 66 条(同步访问共享的可变数据)这一小节中,有这么一个程序:


你觉得这个程序会怎么执行呢?


书里面说:也许你可能期望这个程序运行大概一秒钟左右,之后主线程将

stopRequested 设置为 true,致使后台线程的循环停止。但是在我的机器上,这个程序永远不会终止:因为后台线程永远在循环


问题在于,由于没有同步,就不能保证后台线程何时“看到”主线程对 stopRequested 的值所做的改变。


没有同步,所以虚拟机会将这个代码变成下面这个样子:


书里面是这样说的:


书里提到了一个活性失败的概念:多线性并发时,如果 A 线程修改了共享变量,此时 B 线程感知不到此共享变量的变化,叫做活性失败。

如何解决活性失败呢


让两个线程之间对共享变量有 happens-before 关系,最常用的操作就是volatile 或 加锁。


活性失败的知识点记下来就行,不是这里的重点,重点是下面。


书里说:这是可以接受的,这种优化称作提升(hoisting)。


说起提升这两字,我联想不出来啥,但是看到 hoisting 这个单词,有点意思了。


电光火石之间,我想到了《深入理解Java虚拟机》描述即时编译(Just In Time,JIT)里说到的一些东西了。


《深入理解Java虚拟机》和《Effective Java》,呼应上了!


虽然《Effective Java》里面没有详细描述这个提升是什么,但是我们有理由相信,它指的就是《深入理解Java虚拟机》里面描述的循环表达式外提(Loop Expression Hoisting)。


而这个提升是 JIT 帮我们做的。


我们还能怎么验证一下这个结论呢?


运行的时候配置下面的参数,其含义是禁止 JIT 编译器的加载:


-Djava.compiler=NONE


还是一样的代码,禁用了 JIT 的优化。程序正常运行结束了。


结合上面的描述,再加上这个“循环表达式外提”。现在,你应该就能品出点味道来了。

而且,这里还有一个非常非常重要的信息我可以品出来。


一个没有被 volatile 修饰的变量 stopRequested ,在子线程和主线程中都有用到的时候,Java 内存模型只是不能保证后台线程何时“看到”主线程对 stopRequested 的值所做的改变,而不是永远看不见。


加了 volatile,jvm 一定会保证 stopRequested 的可见性。


不加 volatile,jvm 会尽量保证 stopRequested 的可见性。


也许你会问了,从左边到右边的提升到底是怎么回事,能细致一点,底层一点吗?


当然可以啊。可以深入到汇编语言去。具体怎么操作,你看R大的这两个链接,非常之硬核,虽然可能看不懂,但是看着看着就是想磕头,不读三遍以上,你可能根本不知道他在说什么:


https://hllvm-group.iteye.com/group/topic/34932
https://www.iteye.com/blog/rednaxelafx-644038


我直接说个R大的结论:


所以,这里再次回到文章开始的时候说的点:根据不同的机器、不同的JVM、不同的CPU可能会产生不一样的效果。


但是由于我们绝大部分同学都使用的是 HotSpot 的 Server 模式,所以,运行结果都一样。


在这一小节的最后,我们回到本文[先出个题]环节抛出的那个程序:


这个地方的 while 循环和上面的如出一辙。所以你知道为什么这个程序为什么不会正常结束了吗?


你不仅知道了,而且你还可以回答的比 volatile 更深入一点。


由于变量 flag 没有被 volatile 修饰,而且在子线程休眠的 100ms 中, while 循环的 flag 一直为 false,循环到一定次数后,触发了 jvm 的即时编译功能,进行循环表达式外提(Loop Expression Hoisting),导致形成死循环。而如果加了 volatile 去修饰 flag 变量,保证了 flag 的可见性,则不会进行提升。


比如下面的程序,注释了 14 行和 16 行,while 循环,循环了3359次(该次数视机器情况而定)后,就读到了 flag 为 true,还没有触发即时编译,所以程序正常结束。


输出语句


接下来,我们看输出语句对这个程序的影响:


首先,我们知道了,在第 24 行加入输出语句后,这个程序是会正常结束的。


经过我们上面的分析,我们也可以推导出。加了输出语句后 JVM 并没有做 JIT。

点进 println 方法,可以看到该方法内部是调用了 synchronized 的。


关于这个问题,我需要分三个角度去讨论:


角度一 - stack overflow


在 stack overflow 中找到了这个地址:


https://stackoverflow.com/questions/25425130/loop-doesnt-see-value-changed-by-other-thread-without-a-print-statement?noredirect=1&lq=1


和我们这里的问题,如出一辙。该问题下面有一个回答,非常的好,得到了大家的一致好评:


该回答从现象到原理,再到解决方案都说的头头是道。建议你去阅读一下。

我这里只解析和本文相关的输出语句部分的回答:


我结合自己的理解和这个回答来解释一下:


同步方法可以防止在循环期间缓存 pizzaArrived(就是我们的stop)。


严格的说,为了保证变量的可见性,两个线程必须在同一个对象上进行同步。如果某个对象上只有一个线程同步操作,通过 JIT 技术,JVM 可以忽略它(逃逸分析、锁消除)。


但是,JVM 不够聪明,它无法证明其他线程在设置 pizzaArrived 之后不会调用 println,因此它只能假定其他线程可能会调用 println。(所以有同步操作)


因此,如果使用 System.out.println, JVM 将无法在循环期间缓存变量。


这就是为什么,当有 print 语句时,循环可以正常结束,尽管这不是一个正确的操作。

目录
相关文章
|
7月前
|
设计模式 算法 程序员
程序员为何需要反复修改Bug?探寻代码编写中的挑战与现实
作为开发者,我们在日常开发过程中,往往会遇到反复修改bug的情况,而且不能一次性把代码写的完美无瑕,其实开发项目是一项复杂而富有挑战性的任务,即使经验丰富的程序员也难以在一次性编写完美无瑕地完成代码,我个人觉得一次性写好代码是不可能完成的事情。虽然在设计之初已经尽力思考全面,并在实际操作中力求精确,但程序员仍然需要花费大量时间和精力来调试和修复Bug。那么本文就来分享程序员需要反复修改Bug的原因,以及在开发中所面临的复杂性与挑战。
180 1
程序员为何需要反复修改Bug?探寻代码编写中的挑战与现实
|
敏捷开发 前端开发 开发者
想要成为软件开发中的王者,需要明白的 21 条准则
想要成为软件开发中的王者,需要明白的 21 条准则
|
测试技术
解决Bug应有的心态和解决方法的一些思路、方法和心得
永远要相信程序是不会骗你的,是自己在处理理逻辑中出问题,而在特定的环境中才会出现或者是自己压根就想不到情况下出现。 前几天在处理一个接口任务时,在测试环境跑是一点都没有,但在正式环境却没有将数据拉下来。没有报任何错误,一度怀疑、抱怨! 还好最后找到问题解决了!
88 0
|
7月前
|
人工智能 算法 前端开发
想转行IT,有前途嘛?30个详细理由中会得到你想要的答案
想转行IT,有前途嘛?30个详细理由中会得到你想要的答案
125 0
|
程序员
接受平庸,特别是程序员
接受平庸,特别是程序员
|
人工智能 小程序
行动派:想到就做,无关乎与成功或失败,重在过程!
行动派:想到就做,无关乎与成功或失败,重在过程!
187 0
发散性思维(一)——通过变通和联想来解决问题
发散性思维(一)——通过变通和联想来解决问题
115 0
发散性思维(一)——通过变通和联想来解决问题
|
安全 Java
一个困扰我122天的技术问题,我好像知道答案了。 (1)
一个困扰我122天的技术问题,我好像知道答案了。 (1)
111 0
|
安全 Java 编译器
一个困扰我122天的技术问题,我好像知道答案了。 (3)
一个困扰我122天的技术问题,我好像知道答案了。 (3)
109 0