推荐阅读
- 学习笔记 《 深入理解 Java 虚拟机》
- 学习笔记 《 后端架构设计》
- 学习笔记 《 Java 基础知识进阶》
- 学习笔记 《 Nginx 学习笔记》
- 学习笔记 《 前端开发杂记》
- 学习笔记 《 设计模式学习笔记》
- 学习笔记 《 DevOps 最佳实践指南》
- 学习笔记 《 Netty 入门与实战》
- 学习笔记 《 高性能MYSQL》
- 学习笔记 《 JavaEE 常用框架》
- 学习笔记 《 Java 并发编程学习笔记》
- 学习笔记 《 分布式系统》
- 学习笔记 《 数据结构与算法》
happens-before是JMM最核心的概念。对应Java程序员来说,理解happens-before是理解 JMM的关键。本章中笔者将用最白话的文字来阐述到底什么是 happens-before 原则以及其作用。
1、矛盾点
对于开发人员而言,我们希望内存模型易于理解易于编程,因此期待一个强内存模型来实现代码。但对于编译器和处理器而言,其希望内存模型对其束缚越少越好,这样其就可以利用一些手段优化代码执行,提升效率,编译器和处理器希望提供的是一个弱内存的模型。
所以,基于这矛盾点,JSR-133 的专家们就需要在这两者中间找到一个合理的平衡点:
-
要为开发人员找到简单易用,且提供了足够强的内存可见性
-
同时也要对编译器和处理尽可能的宽松,尽量利用编译器和处理的优化能力
观察下面的代码示例
double PI = 3.1415; // 操作 A
double r = 3; // 操作 B
double area = PI * r * r; // 操作 C
一个强内存模型所期望的原则是:
- A happens-before B
- B happens-before C
- A happens-before C
在这三个规则中,我们知道规则2和3 是必须的,但是规则1则没有必要。因此JMM把 happend-before 禁止重排序的规则分为了会改变结果的重排序以及不会改变结果的重排序。JMM 对于前者则禁止编译器和处理器重排序,对于后者JMM不做要求,允许编译器或者处理器进行重排序。
图片引用 《Java 并发编程的艺术 》P126页 图3-33 JMM 设计示意图
JMM 向程序员提供的 happens-before 规则能满足程序员的需求。JMM 的 happens-before 规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证。同时 JMM 对编译器和处理器的束缚已经尽可能少。从上面的分析可以看出,JMM其实是在遵 循一个基本原则: ** ****只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序), 编译器和处理器怎么优化都行。**
2、happens-before 定义
JSR-133使用 happens-before 的概念来指定两个操作之间的执行顺序(这两个操作可以是同一个线程,也可以是跨线程)。因此,JMM可以通过 happens-before 关系向开发人员提供跨线程的内存可见性保证。
《JSR-133:Java Memory Model and Thread Specification》对happens-before关系的定义如下。
-
如果一个操作A happens-before 另一个操作B,那么操作A的执行结果将对操作B可见,而且操作A的执行顺序排在操作B之前。
-
两个操作之间存在 happens-before 关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系 来执行的结果一致,那么这种重排序并不非法。
总结来说所谓的 happens-before 原则就是: JMM 提供给开发人员的一个简单易于理解的一种强内存模型,如果 操作A happens-before 操作B,那么 操作A所造成的影响(比如修改数据)对B立刻可见。同时也给编译和处理器最大的宽松进行优化的可能
3、happens-before 规则
《JSR-133:Java Memory Model and Thread Specification》定义了如下happens-before规则。
- 程序顺序规则: 一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则: 对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则: 对一个volatile域的写,happens-before于任意后续对这个volatile域的 读。
- 传递性: 如果A happens-before B,且B happens-before C,那么A happens-before C
- start()规则:如果线程A执行操作ThreadB.start(),那么A线程的ThreadB.start() 操作happens-before于线程B中的任意操作。
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作 happens-before于线程A从ThreadB.join() 操作成功返回。
笔者注: 对于Lock锁的实现而言,其实现的 happens-before 原则的本质并不是2,而是3,这是因为在Lock的实现中,是基于 AQS 实现的,通过修改AQS中的 volatile 类型的 state 变量来达到 heppens-before 的原则。
这里我们举两个例子来说明 heppens-before 原则对于开发人员作出的强内存模型的保证。
- 对于原则 5 而言: 我们在主线程中对一些数据操作,随后启动子线程A,那么在主线程中对于变量的操作,对于子线程A而言都是可见的,基于此我们可以不必考虑内存模型的问题。
- 同样的对于原则 3 而言,对一个 volatile 类型的变量而言,是对于 volatile 读线程可见的,这就保证了线程A修改 volatile 变量,其他线程可以立即读取到这个 volatile 变量,这就是 JMM 提供的强内存的模型特性。
4、 总结
再次总结下: 什么是happens-before 原则? 它是 JMM 提供给开发人员的一个简单易于理解的一种强内存模型,只要操作A happens-before 操作B,那么 操作A 所造成的影响(比如修改数据)对操作B立刻可见,即使操作A和操作B不在同一个线程中,另外其也给编译和处理器最大的宽松进行优化的可能。