可见性有序性,Happens-before来搞定
写在前面
上一篇文章并发 Bug 之源有三,请睁大眼睛看清它们 谈到了可见性/原子性/有序性
三个问题,这些问题通常违背我们的直觉和思考模式,也就导致了很多并发 Bug
- 为了解决 CPU,内存,IO 的短板,增加了缓存,但这导致了可见性问题
- 编译器/处理器
擅自
优化 ( Java代码在编译后会变成 Java 字节码, 字节码被类加载器加载到 JVM 里, JVM 执行字节码, 最终需要转化为汇编指令在 CPU 上执行) ,导致有序性问题
初衷是好的,但引发了新问题,最有效的办法就禁止缓存和编译优化,问题虽然能解决,但「又回到最初的起点,呆呆地站在镜子前」是很尴尬的,我们程序的性能就堪忧了.
解决方案
- 作为我们程序猿不想写出 bug 影响 KPI,所以希望内存模型易于理解、易于编程。这就需要基于一个强内存模型来编写代码
- 作为编译器和处理器不想让外人说它处理速度很慢,所以希望内存模型对他们束缚越少越好,可以由他们擅自优化,这就需要基于一个弱内存模型
俗话说:「没有什么事是开会解决不了的,如果有,那就再开一次」😂
JSR-133 的专家们就有了新想法,既然不能完全禁止缓存和编译优化,那就按需禁用缓存和编译优化,按需就是要加一些约束,约束中就包括了上一篇文章简单提到过的 volatile,synchronized,final 三个关键字,同时还有你可能听过的 Happens-Before 原则(包含可见性和有序性的约束),Happens-before 规则也是本章的主要内容
为了满足二者的强烈需求,照顾到双方的情绪,于是乎: JMM 就对程序猿说了一个善意的谎言: 「会严格遵守 Happpen-Befores 规则,不会重排序」让程序猿放心,私下却有自己的策略:
- 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
- 对于不会改变程序执行结果的重排序, JMM对编译器和处理器不做要求 (JMM允许这种重排序)。
我们来用个图说明一下:
这就是那个善意的谎言,虽是谎言,但还是照顾到了程序猿的利益,所以我们只需要了解 happens-before 规则就能得到保证 (图画了好久,不知道是否说明了谎言的所在😅,欢迎留言)
Happens-before
Happens-before 规则主要用来约束两个操作,两个操作之间具有 happens-before 关系, 并不意味着前一个操作必须要在后一个操作之前执行,happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见, (the first is visible to and ordered before the second)
说了这么多,先来看一小段代码带你逐步走进 Happen-Befores 原则,看看是怎样用该原则解决 可见性 和 有序性 的问题:
class ReorderExample { int x = 0; boolean flag = false; public void writer() { x = 42; //1 flag = true; //2 } public void reader() { if (flag) { //3 System.out.println(x); //4 } } }
假设 A 线程执行 writer 方法,B 线程执行 reader 方法,打印出来的 x 可能会是 0,上一篇文章说明过: 因为代码 1 和 2 没有数据依赖关系,所以可能被重排序
flag = true; //2 x = 42; //1
所以,线程 A 将 flag = true
写入但没有为 x 重新赋值时,线程 B 可能就已经打印了 x 是 0
那么为 flag 加上 volatile 关键字试一下:
volatile boolean flag = false;
即便加上了 volatile 关键字,这个问题在 java1.5 之前还是没有解决,但 java1.5 和其之后的版本对 volatile 语义做了增强,问题得以解决,这就离不开 Happens-before 规则的约束了,总共有 6 个规则,且看
程序顺序性规则
一个线程中的每个操作, happens-before 于该线程中的任意后续操作
第一感觉这个原则是一个在理想状态下的"废话",并且和上面提到的会出现重排序的情况是矛盾的,注意这里是一个线程中的操作,其实隐含了「as-if-serial」语义: 说白了就是只要执行结果不被改变,无论怎么"排序",都是对的
这个规则是一个基础规则,happens-before 是多线程的规则,所以要和其他规则约束在一起才能体现出它的顺序性,别着急,继续向下看