前两天一个小伙伴忽然找我求助,说准备换个坑,最近在系统学习多线程知识,但遇到了一个刷新认知的问题……java
做为阅读福利,小编也整理一些Java笔记资料(包含面试真题+脑图+手写pdf)须要的自行领取~git
最全学习笔记大厂真题+微服务+MySQL+分布式+SSM框架+Java+Redis+数据结构与算法+网络+Linux+Spring全家桶+JVM+高并发+各大学习思惟脑图+面试集合面试
小伙伴:Effective JAVA 里的并发章节里,有一段关于可见性的描述。下面这段代码会出现死循环,这个我能理解,JMM 内存模型嘛,JMM 不保证 stopRequested 的修改能被及时的观测到。算法
static boolean stopRequested = false; public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(() -> { int i = 0; while (!stopRequested) { i++; } }) ; backgroundThread.start(); TimeUnit.MICROSECONDS.sleep(10); stopRequested = true ; }
但奇怪的是在我加了一行打印以后,就不会出现死循环了!难道我一行 println 能比 volatile 还好使啊?这俩也不要紧啊express
static boolean stopRequested = false; public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(() -> { int i = 0; while (!stopRequested) { // 加上一行打印,循环就能退出了! System.out.println(i++); } }) ; backgroundThread.start(); TimeUnit.MICROSECONDS.sleep(10); stopRequested = true ; }
我:小伙子八股文背的挺熟啊,JMM 张口就来。 ?网络
我:这个……实际上是 JIT 干的好事,致使你的循环没法退出。JMM 只是一个逻辑上的内存模型,内部有些机制是和 JIT 有关的数据结构
好比你第一个例子里,你用-Xint
禁用 JIT,就能够退出死循环了,不信你试试?多线程
小伙伴:卧槽,真的能够,加上 -Xint 循环就退出了,好神奇!JIT 是个啥啊?还能有这种功效?并发
众所周知,JAVA 为了实现跨平台,增长了一层 JVM,不一样平台的 JVM 负责解释执行字节码文件。虽然有一层解释会影响效率,但好处是跨平台,字节码文件是平台无关的。
在 JAVA 1.2 以后,增长了 即时编译(Just-in-Time Compilation,简称 JIT) 的机制,在运行时能够将执行次数较多的热点代码编译为机器码,这样就不须要 JVM 再解释一遍了,能够直接执行,增长运行效率。
?
但 JIT 编译器在编译字节码时,可不只仅是简单的直接将字节码翻译成机器码,它在编译的同时还会作不少优化,好比循环展开、方法内联等等…… ?
这个问题出现的缘由,就是由于 JIT 编译器的优化技术之一 - 表达式提高(expression hoisting) 致使的。
先来看个例子,在这个 hoisting
方法中,for 循环里每次都会定义一个变量 y
,而后经过将 x*y 的结果存储在一个 result 变量中,而后使用这个变量进行各类操做
public void hoisting(int x) { for (int i = 0; i < 1000; i = i + 1) { // 循环不变的计算 int y = 654; int result = x * y; // ...... 基于这个 result 变量的各类操做 } }
可是这个例子里,result 的结果是固定的,并不会跟着循环而更新。因此彻底能够将 result 的计算提取到循环以外,这样就不用每次计算了。JIT 分析后会对这段代码进行优化,进行表达式提高的操做:
public void hoisting(int x) { int y = 654; int result = x * y; for (int i = 0; i < 1000; i = i + 1) { // ...... 基于这个 result 变量的各类操做 } }
这样一来,result 不用每次计算了,并且也彻底不影响执行结果,大大提高了执行效率。
注意,编译器更喜欢局部变量,而不是静态变量或者成员变量;由于静态变量是“逃逸在外的”,多个线程均可以访问到,而局部变量是线程私有的,不会被其余线程访问和修改。 ?
编译器在处理静态变量/成员变量时,会比较保守,不会轻易优化。 ?
像你问题里的这个例子中,stopRequested
就是个静态变量,编译器本不该该对其进行优化处理;
static boolean stopRequested = false;// 静态变量 public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(() -> { int i = 0; while (!stopRequested) { // leaf method i++; } }) ; backgroundThread.start(); TimeUnit.MICROSECONDS.sleep(10); stopRequested = true ; }
但因为你这个循环是个 leaf method
,即没有调用任何方法,因此在循环之中不会有其余线程会观测到stopRequested
值的变化。那么编译器就冒进的进行了表达式提高的操做,将stopRequested
提高到表达式以外,做为循环不变量(loop invariant)处理:
int i = 0; boolean hoistedStopRequested = stopRequested;// 将stopRequested 提高为局部变量 while (!hoistedStopRequested) { i++; }
这样一来,最后将 stopRequested
赋值为 true 的操做,影响不了提高的hoistedStopRequested
的值,天然就没法影响循环的执行了,最终致使没法退出。 ?
至于你增长了 println
以后,循环就能够退出的问题。是由于你这行 println 代码影响了编译器的优化。println 方法因为最终会调用 FileOutputStream.writeBytes 这个 native 方法,因此没法被内联优化(inling)。而未被内敛的方法调用从编译器的角度看是一个“full memory kill”,也就是说 反作用不明 、必须对内存的读写操做作保守处理。 ?
在这个例子里,下一轮循环的 stopRequested
读取操做按顺序要发生在上一轮循环的 println 以后。这里“保守处理”为:就算上一轮我已经读取了 stopRequested
的值,因为通过了一个反作用不明的地方,再到下一次访问就必须从新读取了。 ?
因此在你增长了 prinltln 以后,JIT 因为要保守处理,从新读取,天然就不能作上面的表达式提高优化了。 ?
以上对表达式提高的解释,总结摘抄自 R大的知乎回答。 ?
我:“这下明白了吧,这都是 JIT 干的好事,你要是禁用 JIT 就没这问题了”
小伙伴:“卧槽????,一个简单的 for 循环也太多机制了,没想到 JIT 这么智能,也没想到 R 大这么????”
小伙伴:“那 JIT 必定不少优化机制吧,除了这个表达式提高还有啥?”
我:我也不是搞编译器的……哪了解这么多,就知道一些经常使用的,简单给你说说吧
和表达式提高相似的,还有个表达式下沉的优化,好比下面这段代码:
public void sinking(int i) { int result = 543 * i; if (i % 2 == 0) { // 使用 result 值的一些逻辑代码 } else { // 一些不使用 result 的值的逻辑代码 } }
因为在 else 分支里,并无使用 result 的值,可每次无论什么分支都会先计算 result,这就不必了。JIT 会把 result 的计算表达式移动到 if 分支里,这样就避免了每次对 result 的计算,这个操做就叫表达式下沉:
public void sinking(int i) { if (i % 2 == 0) { int result = 543 * i; // 使用 result 值的一些逻辑代码 } else { // 一些不使用 result 的值的逻辑代码 } }
学习技术必定要制定一个明确的学习路线,这样才能高效的学习,没必要要作无效功,既浪费时间又得不到什么效率,你们不妨按照我这份路线来学习。
你们不妨直接在牛客和力扣上多刷题,同时,我也拿了一些面试题跟你们分享,也是从一些大佬那里得到的,你们不妨多刷刷题,为金九银十冲一波!
最后,若须要完整pdf版,能够点赞本文后点击这里免费领取