众所周知,编程是一门玄学。
本文主要是描述输出语句、sleep以及Integer对线程安全的影响。第一次碰到这个问题是122天之前,当时就觉得很奇怪。
至于为什么还有Integer?我也不知道,可能是玄学吧! 这也是本文最后留下的一个问题,如果有知道的朋友还请指点一二。
荒腔走板聊生活
首先,还是本号特色,先荒腔走板的聊聊生活。
上面这张图是我 2017 年 12 月 9 日在北京西山国家森林公园拍的。
拍照的地方有个很有意思的名字:鬼笑石。
我在北京待了三年,这个地方一共只去了两次,这是第一次去的时候拍的,我一个人从香山走到了西山,那个时候还是一个充满斗志的北漂。
第二次去是因为我感觉自己可能要离开北京了,如果说在离开之前还能去一个地方留恋一下,“鬼笑石”算得上其中之一。于是约了好几个朋友一起再爬了一次。
在这个地方一眼望去,你能站在五环边上,看到大半个北京,从夕阳西下,倦鸟归林看到华灯初上,万家灯火。
你可以感受到在偌大的北京中自己的渺小,也能感受到在这么大的北京,一定要好好拼命努力才能不负北漂的时光。
两次我都在听同一首歌赵雷的《理想》:
公车上我睡过了车站 一路上我望着霓虹的北京 我的理想把我丢在这个拥挤的人潮 车窗外已经是一片白雪茫茫 ...... 理想今年你几岁 你总是诱惑着年轻的朋友 你总是谢了又开 给我惊喜 又让我沉入失望的生活里 ...... 理想永远都年轻 你让我倔强地反抗着命运 你让我变得苍白 却依然天真的相信花儿会再次的盛开
歌词写的真好,赵雷唱的真好,以至于我往后的每一次听到这首歌的时候,我都会想起北漂的那些日子。
每次有读者私聊我说,他要开始北漂啦。我都会说:一定要好好珍惜、把握、不虚度北漂的每一天。
这次,我再分享两首歌给你吧。赵雷的《理想》和李志的《热河》。
好了,说回文章。
本文主要是描述输出语句、sleep 以及 Integer 对线程安全的影响。
为什么还有 Integer ?我也不知道,可能是玄学吧!
先出个题
这个程序的意思就是定义一个 boolean 型的 flag 并设置为 false。主线程一直循环,直到 flag 变为 true。
而 flag 什么时候变为 true 呢?
从程序里看起来是在子线程休眠 100ms 后,把 flag 修改为 true。
来,你说这个程序会不会正常结束?
但凡是对 Java 并发编程有一定基础的朋友都能看出来,这个程序是一个死循环。导致死循环的原因是 flag 变量不是被 volatile 修饰的,所以子线程对 flag 的修改不一定能被主线程看到。
而这个地方,如果是在 HotSpot jvm 中用 Server 模式跑的程序,是一定不会被主线程看到,原因后面会讲。
如果你对于 Java 内存模型和 volatile 关键字的作用不清楚的话,我建议你先赶紧去搜一下相关的知识点,补充一下后再来看这篇文章。
由于 Java 内存模型和 volatile 关键字是面试常见考题,出现的几率非常之高,所以已经有很多的文章写过了,本文不会对这些基本概念进行解释。
我默认你是了解 Java 内存模型和 volatile 关键字的作用的。
我第一次遇到这个问题,是在 2019 年 11 月 19 日,距今天已经122天了。我常常在夜里想起这个题以及这个题的变种问题,为什么呢?到底是为什么呢?
我再给你提供一个可以直接复制粘贴运行的版本,我建议文中的代码你都去执行一遍,你就会知道:MD,这事儿真是绝了!
public class VolatileExample { private static boolean flag = false; private static int i = 0; public static void main(String[] args) { new Thread(() -> { try { TimeUnit.MILLISECONDS.sleep(100); flag = true; System.out.println("flag 被修改成 true"); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); while (!flag) { i++; } System.out.println("程序结束,i=" + i); }
}
还有,需要事先说明的是:要让程序按照预期结束的正常操作是用 volatile 修饰 flag 变量。但是这题要是加上 volatile 就没有意思了,也就失去了探索的意义。
所以下面的这些骚操作,仅做研究,真实场景中不能这样去做。
另外,需要说明的是,根据不同的机器、不同的JVM、不同的CPU可能会产生不一样的效果。
遇事不决,量子力学
我会在这一小节基于上面展示的程序进行三次非常小的变化。
相信我,绝对让你懵逼。甚至让你觉得:不可能吧?我得亲自操作一下。
操作之后你就会说:卧槽,还真是这样?这是量子力学吗?
第一次程序改造
那我把上面这题变一下,改变成下面这样:
仅仅在程序的第 24 行加入了一个输出语句,用于输出每次循环时 flag 的值。其他地方没有任何变化。
可以看到 idea 在 24 行还给了我们一个友情提示:
它说:flag is always false。
来,你再猜一下。这个程序还是不是死循环呢?
执行之后你会发现,这个程序居然正常结束了,但是你不知道为什么,你只能大喊一声:卧槽,绝了!
或者你说你知道,因为输出语句里面有 synchronized 关键字。
很好,别着急,接着往下看。看看会不会被打脸。
第二次程序改造
先接着看下面的程序:
这次的变动点是在 while 循环里面加了一个 10ms 的睡眠。
来,你再猜一下。这个程序还是不是死循环呢?
执行之后你会发现,这个程序居然正常结束了,但是你也不知道为什么,你只能再次大喊一声:卧槽,这TM绝了!
sleep 语句里面没有 synchronized 了吧,你再给我解释一波?
也许你会说,这我也知道,sleep 会导致内存的刷新操作。
来,等会把你的另外一半脸伸过来挨打。
第三次程序改造
再看这一个改造程序:
这次的改动点是在第 9 行,用 volatile 修饰了变量 i。注意啊,flag 变量还是没有用 volatile 修饰的。
在 23 行,idea 又给了一个友情提示:
对于 volatile 修饰的字段 i 进行了非原子性的操作。
但是,没有关系,朋友们,这个题的考点不在于此,好吗?
你只需要知道对于 volatile 修饰的变量 i,进行 i++ 操作是不对的,因为 volatile 只保证可见性,不保证原子性,而 i++ 操作就不是原子操作的。
来,你再猜一下。上面这个程序还是不是死循环呢?
执行之后你会发现,这个程序居然正常结束了,但是你还是不知道为什么,你只能再次大喊一声:卧槽,真TM绝了!