重排序、happens-before、JMM设定
2020年12月4日
22:19
目录 |
- 1. 指令重排序 - 2. 顺序一致性和JMM的保证 - 2.1 顺序一致性模型 - 2.2 JMM的保证 - 同步程序的顺序一致性 - 未同步程序的顺序一致性 - 对比可以加深对二者的理解。 - 3. happens-before关系
|
单线程内JAVA保证结果一致,统一重排序,多线程内需要正确同步。
1. 指令重排序
这个,就是组成原理中的指令流水。一个图就能够弄懂,原理是对指令读、写、操作等切分,然后重排序,分工的原理,提高所有硬件的利用率。
底层指令重排有三种方式:
- 编译器优化重排:不改变语义,编译器重排。
- 指令并行重排:若指令之间没有依赖,则处理器重排并行
- 内存系统重排:指多级缓存中存在同步时间差的一些问题。
指令重排保证了串行语义的一致性,但多线程就没有义务了。
2. 顺序一致性和JMM的保证
JMM设计理念对照前者理解,可能能获得更深的认识。
2.1 顺序一致性模型
这是一个理论理想模型,通常被用来设计参考用。
定义两大特征:
- 一个线程中的操作必须顺序
- 所有线程都只能看到单一顺序,每个线程的操作都是原子的,可见的。(反例:本地内存操作)
2.2 JMM的保证
同步程序的顺序一致性
JMM的实现是,保证执行结果一致的情况下允许编译器和处理器的任何优化。
具体而言:
- 对于临界区内的程序,可以进行重排,但需要保证结果,即,单线程执行一致性
- 完成临界区操作之后,JMM会特殊处理,保证临界区的可见性
未同步程序的顺序一致性
JMM仍然是 原来的JMM,它从来不保证程序的顺序执行,从来不保证多线程下结果的一致性。JMM提供最小安全性和最小的保证:
- 线程读取到的值不可能无中生有,必须被赋值,其他程序或者默认值
- JMM保证单线程内结果一致性,但从来不保证顺序
对比
对比可以加深对二者的理解。
顺序一致性模型 | JMM的保证 |
单线程内程序顺序保证 | 单线程内结果一致性保证 |
所有线程操作可见性,一致性 | 不保证所有线程的看到的顺序一致性(这是由于本地内存,JMM操作不一定可见) |
对内存的读写操作具备原子性 | 64位的long和double不保证原子性(?) |
3. happens-before关系
程序员需要JMM提供一个强的内存模型来编写代码;另一方面,编译器和处理器希望JMM对它们的束缚越少越好,这样它们就可以最可能多的做优化来提高性能,希望的是一个弱的内存模型。
来自 <http://concurrent.redspider.group/article/02/7.html>
JMM的观点是,只要不改变程序结果(单线程的程序或者正确同步的多线程),编译器和处理器怎么优化都可以。
happens-before是这样一种抽象规则,非常简单,但只要遵循hb规则,就能保证程序强内存可见性。可以想象成有向图A->B:
- 首先,操作A对B具有可见性
- 保证重排序之后A->B的结果一致性
下面通过一些天然的hb关系加深对概念的理解。
- 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。 | JMM的设计,保证了单线程内程序结果一致性——也可以说符合代码顺序,可见,一致。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。| 锁的底层是mutex互斥量,解锁加锁是强顺序,所以它天然符合hb,可见,一致
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。| 值得注意,java保证volatile声明的变量,写之后必定进行内存可见,也就是它是一个绝对同步的信号量
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。| A -> B B -> C A -> C
- start规则:如果线程A执行操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作 |B不start能启动A?没有鸡哪有蛋,强顺序,天然hb
- join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。| join规定,A会等待B执行完成,那么A -> B是强顺序,天然hb
来自 <http://concurrent.redspider.group/article/02/7.html>