happens-before是JMM最核心的概念。对应Java程序员来说,理解 happens-before是理解JMM的关键。

我的并发系列文章,前面三篇学习了 Java并发机制底层实现的三个关键要素:volatilesynchronized原子性操作。以及Java内存模型是为了解决在并发环境下由于 CPU缓存、编译器和处理器的指令重排序 导致的可见性、有序性问题。 其中重点学习了 volatile 的内存语义,以及JMM是如何定义和实现的,在学习 volatile 内存语义实现原理时我们了解到了 JMM 解决指令重排其实是定义了一项 happens-before 规则,今天我们就来一窥究竟,尽量以通俗易懂的话语带大家学习 happens-before 。

如果还没有阅读 前面几篇关于 Java内存模型学习的文章,建议先翻到文末点击对应链接阅读,循序渐进比较好一点。

接下来,就进入主题,开始今天的表演。

JMM的设计

要学习 happens-before 这里首先介绍下JMM的设计意图。这个问题首先从实际出发:

  1. 我们程序员写代码时,是要求内存模型易于理解,易于编程,所以我们需要依赖一个强内存模型来编码。 也就是说向公理一样,定义好的规则,我们遵守规则写代码就完事了。
  2. 对于编译器和处理器的实现来说,它们希望约束尽量少一些,毕竟你限制它们肯定影响它们的执行效率,不能让他们尽己所能的优化来提供性能。所以他们需要一个弱内存模型。

好了,上面谈到的这两点明显就是冲突的,作为程序员我们希望JMM提供给我们一个强内存模型,而底层的编译器和处理器又需要一个弱内存模型来提高自己的性能。

在计算机领域,这种需要权衡的场景非常多,比如内存和CPU寄存器,就引入了CPU多级缓存来解决性能问题,不过也引入了多核cpu并发场景下的各种问题。 所以这里也一样,我们需要找到一个平衡点,来满足程序员的需求,同时也尽可能满足编译器和处理器限制放松,性能最大化。

因此JMM在设计时,定义了如下策略:

  1. 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
  2. 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)。

下面结合这副JMM设计图来理解下:

从上图中可以看到,JMM向我们程序员提供了足够强的内存可见性保证,在不影响程序执行结果的情况下,有些可见性保证并一定存在,比如下面的程序,A happens-before B 并不保证,因为其不影响程序执行结果;

double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C

这就引出了另一个方面,JMM为了满足编译器和处理器的约束尽可能少,它遵循的规则是:只要不改变程序的执行结果,编译器和处理器想怎么优化就怎么优化。例如,如果编译器经过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除。再如,如果编译器经过细致的分析后,认定一个volatile变量只会被单个线程访问,那么编译器可以把这个volatile变量当作一个普通变量来对待。这些优化既不会改变程序的执行结果,又能提高程序的执行效率。

happens-before 规则

如何 理解 happens-before规则呢? 如果仅仅望文生义理解为先行发生,那么南辕北辙了。happens-before 表达的并不是说前面一个操作发生在后面一个操作的前面,尽管从程序员编程角度来看也并不会出错,但它其实表达的是,前一个操作的结果对后续操作时可见的

你可能会问这两种说法有什么区别呢?

这是因为JMM为程序员提供的视角就是按顺序执行的,且满足一个操作 happens-before 于另一个操作,那么第一个操作的执行结果将对第二个执行结果可见,而且第一个操作的执行顺序排在第二个顺序之前。注意,这是 JMM向程序员做出的保证

但其实,JMM在对编译器和处理器进行约束时,如前面所说,遵循的规则是:再不改变程序执行结果的前提下,编译器和处理器怎么优化都行。也就是说两个操作之间存在 happens-before 规则Java平台并不一定按照规则定义的顺序来执行。 这么做的原因是因为,我们程序员并不关心两个操作是否被重排序,只要保证程序执行时语义不能改变就好了。

happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

理解了 happens-before 的含义后,我们一起来看下具体的 happens-before 规则定义。

1.程序顺序规则

一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。这个还是非常好理解的,比如上面那三行代码,第一行的 "double pi = 3.14; " happens-before 于 “double r = 1.0;”,这就是规则1的内容,比较符合单线程里面的逻辑思维,很好理解。

double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C

2. 监视器锁规则

对一个锁的解锁,happens-before于随后对这个锁的加锁。

这个规则中说的锁其实就是Java里的 synchronized。例如下面的代码,在进入同步块之前,会自动加锁,而在代码块执行完会自动释放锁,加锁以及释放锁都是编译器帮我们实现的。

synchronized (this) { //此处自动加锁// x是共享变量,初始值=10if (this.x < 12) {this.x = 12; }
} //此处自动解锁

所以结合锁规则,可以理解为:假设 x 的初始值是 10,线程 A 执行完代码块后 x 的值会变成 12(执行完自动释放锁),线程 B 进入代码块时,能够看到线程 A 对 x 的写操作,也就是线程 B 能够看到 x==12。这个也是符合我们直觉的,非常好理解。。

3. volatile变量规则

对一个volatile域的写,happens-before于任意后续对这个volatile域的读

这个就有点费解了,对一个 volatile 变量的写操作相对于后续对这个 volatile 变量的读操作可见,这怎么看都是禁用缓存的意思啊,貌似和 1.5 版本以前的语义没有变化啊(前面讲的1.5版本前允许volatile变量和普通变量之间重排序)?如果单看这个规则,的确是这样,但是如果我们关联一下规则 4,你就能感受到变化了

4. 传递性

如果A happens-before B,且B happens-before C,那么A happens-before C。

我们将规则 4 的传递性应用到我们下面的例子中,会发生什么呢?

class VolatileExample {int x = 0;volatile boolean v = false;public void writer() {x = 42;v = true;}public void reader() {if (v == true) {// 这里x会是多少呢?}}
}

可以看下面这幅图:

从图中,我们可以看到:

  1. “x=42” Happens-Before 写变量 “v=true” ,这是规则 1 的内容;
  2. 写变量“v=true” Happens-Before 读变量 “v=true”,这是规则 3 的内容 。
  3. 再根据这个传递性规则,我们得到结果:“x=42” Happens-Before 读变量“v=true”。这意味着什么呢?

如果线程 B 读到了“v=true”,那么线程 A 设置的“x=42”对线程 B 是可见的。也就是说,线程 B 能看到 “x == 42” ,有没有一种恍然大悟的感觉?这就是 1.5 版本对 volatile 语义的增强,这个增强意义重大,1.5 版本的并发工具包(java.util.concurrent)就是靠 volatile 语义来搞定可见性的。

5. start()规则

这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

6. join()规则

如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

通过上面6中 happens-before 规则的组合就能为我们程序员提供一致的内存可见性。 常用的就是规则1和其他规则结合,为我们编写并发程序提供可靠的内存可见性模型。

总结

在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。

JMM的设计分为两部分,一部分是面向我们程序员提供的,也就是happens-before规则,它通俗易懂的向我们程序员阐述了一个强内存模型,我们只要理解 happens-before规则,就可以编写并发安全的程序了。 另一部分是针对JVM实现的,为了尽可能少的对编译器和处理器做约束,从而提高性能,JMM在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。 我们只需要关注前者就好了,也就是理解happens-before规则。毕竟我们是做程序员的,术业有专攻,能写出安全的并发程序就好了。

精彩文章推荐:

死磕Java并发?首先需要学习的并发机制底层实现的三个原理

阿里面试,一面就倒在了Java内存模型上

大厂面试:如何用快排思想在O(n)内查找第K大元素?

happens-before是什么?JMM最最核心的概念,看完你就懂了相关推荐

  1. android 中旋转往下抛动画,《全民解答》第26集:超级详细逆旋转发球教学 几点核心的关键点 看完马上开窍!...

    原标题:<全民解答>第26集:超级详细逆旋转发球教学 几点核心的关键点 看完马上开窍! 简介 观众朋友们大家好,欢迎收看全民解答,今天我们来讲一下逆旋转发球,那么逆旋转可以说是在现在专业比 ...

  2. JMM 最最最核心的概念:Happens-before 原则

    事实上,从 JMM 设计者的角度来看,可见性和有序性其实是互相矛盾的两点: 一方面,对于程序员来说,我们希望内存模型易于理解.易于编程,为此 JMM 的设计者要为程序员提供足够强的内存可见性保证,专业 ...

  3. Istio服务网格进阶①:Istio服务网格核心理论概念

    Istio服务网格核心理论概念 文章目录 Istio服务网格核心理论概念 1.什么是Istio服务网格 1.1.Istio服务网格的介绍 1.2.Istio服务网格微服务治理的功能 1.3.Istio ...

  4. Reinforcement Learning 的核心基础概念及实现

    Reinforcement Learning 的核心基础概念及实现  Machine Learning  |   5  |   5732 2013 年伦敦的一家小公司 DeepMind 发表了一篇论文 ...

  5. Cesium 核心类Viewer-查看器详解

    Cesium 核心类Viewer-查看器详解 1 简介 A base widget for building applications. It composites all of the standa ...

  6. 此计算机核心参数,如何看懂电脑CPU的性能参数?看完这篇就会了

    原标题:如何看懂电脑CPU的性能参数?看完这篇就会了 有人说:"电脑好不好,全看CPU."没错,作为电子计算机的主要设备之一,同时也是电脑中的核心配件,CPU对电脑重要性不言而喻. ...

  7. 云原生核心技术之Istio服务网格核心理论概念(一)

    Istio服务网格核心理论概念 文章目录 Istio服务网格核心理论概念 1.Service Mesh基本概念 1.1.什么是Service Mesh服务网格 1.2.服务网格的特点 1.3.服务网格 ...

  8. 一起学AI:核心基础概念

    一起学AI:核心基础概念 AI基础概念 什么是学习率? batchsize和epoch分别是什么? 梯度消失与梯度爆炸是什么? 什么是过拟合.欠拟合和泛化? 归一化.正则化.标准化是什么? 线性回归和 ...

  9. CS162操作系统课程第二课-4个核心OS概念

    熟肉视频地址: CS162操作系统课程第二课-4个核心OS概念(上) CS162操作系统课程第二课-4个核心OS概念(下) 我们讨论了操作系统如何扮演裁判,魔术师和粘合剂的角色,裁判是指对于资源保护的 ...

最新文章

  1. Libevent调用
  2. 顺序表应用3:元素位置互换之移位算法
  3. 34、Power Query-中国式排名
  4. asp.net 得到上一页地址
  5. 让“学生看得明白” 复旦数学教授在无人教室录课程板书
  6. Spring:Spring IOC注解方式注册beanDefinition
  7. 程序员惨遭辞退竟只因提了些代码修改意见?
  8. oracle 基数 选择率,1.1.2.2 可选择率(1)
  9. 个人理财系统springboot项目开发(一)需求分析文档
  10. 【exp-linux】用aplay直接播放音频文件(mp3,wav)
  11. 【iPhone资料】历代iPhone参数
  12. win7系统服务器停止,win7系统即将停止服务,你的电脑准备好换win10 了吗?
  13. keyshot渲染图文教程_keyshot渲染图文教程,教你如何渲染冰的效果
  14. CSDN论坛如何能够获得积分呢?
  15. 在华为云服务器安装libreoffice软件
  16. html获取随机字母,html5 canvas随机生成英文字母数字组合图片验证码代码
  17. php生成数字和字母的随机组合,php随机生成数字字母组合的方法,php生成数字字母_PHP教程...
  18. windows下连接服务器
  19. CPU的功能和基本结构
  20. flex布局交叉轴方向对齐方式详解

热门文章

  1. SpringCloud和SpringBoot版本冲突问题
  2. ubuntu 配置网络重启 使用/etc/init.d/networking restart 命令无效
  3. Macbook M1 相关软件适配和配置
  4. 进程调度的几种方式与算法简介
  5. 中断处理与特权级转移
  6. Symbian S60 blog
  7. 数据结构与算法设计思路和考察点
  8. Solidworks2022草图标注尺寸和实际尺寸不符如何解决?
  9. 西安尚学堂Java编程笔试面试练习题
  10. 不懂编程,不会数据架构,同事做的可视化报表是如何让我佩服的?