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

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

角度二 - Doug Lea


这个角度其实和角度一基本上一致。但是由于有了 Doug Lea 的加持,所以得单独的再提一下,大佬,必须值得这样的待遇。


在 Doug Lea 写的这本书里:


有一小节专门讲可见性的:



他先说了一句:写线程释放同步锁,读线程随后获取相同的同步锁。


这是我们常规的认知。但是他紧接着说了个 In essence(本质上)。


从本质上来说,线程释放锁的操作,会强制性的将工作内存中涉及的,在释放锁之前的,所有写操作都刷新到主内存中去。


而获取锁的操作,则会强制新的重新加载可访问的值到该线程的工作内存中去。


角度三 - IO操作


第三个角度,和前面说的 synchronized 关系就不大了。


在这个角度里面,解释是这样的:前面我们已经知道了,即使一个变量没有加 volatile 关键字,JVM 会尽力保证内存的可见性。但是如果 CPU 一直处于繁忙状态,JVM 不能强制要求它去刷新内存,所以 CPU 有没办法去保证内存的可见性了。


而加了 System.out.println 之后,由于 synchronized 的存在,导致 CPU 并不是那么的繁忙(相对于之前的死循环而言)。这时候 CPU 就可能有时间去保证内存的可见性,于是 while 循环可以被终止。


(别说锁粗化了,我觉得这个回答肯定是不对的。)


通过上面三个角度的分析,我们能得到两个结论

1.输出语句的 synchronized 的影响。


2.输出语句让 CPU 有时间去做内存刷新的事儿。比如在我的示例中,把输出语句换成new File()的操作也是可以正常结束的。


但是说真的,我也不知道哪个结论是对的,诸君判断吧。


sleep语句


sleep 语句对程序的影响,我给出的例子是这样的:


同样,我在 stack overflow 上也找到了相关问题:


https://stackoverflow.com/questions/42676751/thread-sleep-makes-compiler-read-value-every-time


下面有个回答是这样的:


根据这个回答,我解释一下为什么我们的测试程序没有死循环。


关于 sleep 我们可以看官方文档:


https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.3


文档中的 while 循环中的 done 也是没有被 volatile 修饰的。


里面有两句话特别重要(上面红框圈起来的部分):


1.Thread.sleep 没有任何同步语义(Thread.yield也是)。编译器不必在调用 Thread.sleep 之前将缓存在寄存器中的写刷新到共享内存,也不必在调用 Thread.sleep 之后重新加载缓存在寄存器中的值。


2.编译器可以**自由(free)**读取 done 这个字段仅一次。


特别是第二点,注意文档中的这个 free。简直用的是一发入魂。


自由,意味着编译器可以选择只读取一次,也可以选择每次都去读取,这才是自由的含义。这是编译器自己的选择。


volatile -- 巧合


接着我们看第三个改造点:


改动点是在第 9 行,用 volatile 修饰了变量 i。


如果我们用下面的 jvm 参数运行:


-XX:+UnlockDiagnosticVMOptions 
-XX:+PrintAssembly 
-XX:CompileCommand=dontinline,*VolatileExample.main 
-XX:CompileCommand=compileonly,*VolatileExample.main


可以看到如下输出:


在操作程序的第 23 行,有个 lock 前缀。而这个 lock 指令,就相当于一个内存屏障。会触发 Java 内存模式中的“store”和“write”操作。


这里属于 volatile 的知识点,就不详细说明了。


有的人可能会往 happens-before 的方面去想。很不幸,这个想法是不对的。

为什么呢?


主线程读的是非 volatile 类型的 flag,写的是 volatile类型的 i。但是子线程中只有对非 volatile 类型的 flag 的写入。


来,你怎么去建立起子线程对 flag 的写入 happens-before 于主线程对 flag 的读的关系?


我个人理解这个地方导致程序正常结束的原因是:巧合!


巧合在于,可能由于某个时刻变量 i 和 flag 处于同一 CPU 的 cacheline 中。因为 lock 操作保证变量 i 的可见性的同时把 flag 也刷出去了。


需要特别说明的是:这个地方纯属个人理解,我没有找到相应的资料进行结论的支撑。不具备权威性和引用性。


Integer -- 玄学


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


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

这个程序在我的机器上正常结束了。我真不知道为什么,写出来的目的是万一有读者朋友知道的原因的话,请多多指教。


如果要让我强行给个解释的话,我想会不会是 i++ 操作涉及到的拆箱装箱操作,导致 CPU 有时间去刷了工作内存。


这个程序我再稍稍一变:


注释掉了第九行,在第21行加入 Integer i=0。


是的,它也运行结束了。只是需要一点时间。在i = -2147483648 的时候。


而 -2147483648 就是 Integer.MIN_VALUE:


也许是溢出操作带来的影响。我也不知道。

别问,问就是玄学。


留个坑在这里,希望以后自己能把它填上。也希望知道原因的朋友能给我指点一二,不胜感谢。


最后说一句(求关注)


回到文章最开始说的,其实要让程序按照预期结束的正确操作是用 volatile 修饰 flag 变量。但是这题要是加上 volatile 就没有意思了,也就失去了探索的意义。


再次申明:上面的这些骚操作,仅做研究,真实场景中不能这样去做。


上面的问题关于输出语句和 sleep 对线程安全的影响,其实困扰我很长时间了,从第一次遇见到现在有122天了,这两个问题我现在是比较清楚了。


但是,我在写这篇文章的时候又遇到了上面说的最后一个关于 Integer 的问题。实在是不知道怎么回事。


也许,我可以把这个坑填上吧。


也许,编程的尽头,是玄学吧。


才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。(我每篇技术文章都有这句话,我是认真的说的。)


感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。


我是why技术,一个不是大佬,但是喜欢分享,又暖又有料的四川好男人。


欢迎关注公众号【why技术】,坚持输出原创。分享技术、品味生活,愿你我共同进步。


目录
相关文章
|
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天的技术问题,我好像知道答案了。 (2)
一个困扰我122天的技术问题,我好像知道答案了。 (2)
101 0