引入原因
由于缓存一致性和指令重排序的存在,多线程程序的执行可能会和我们设想的结果不同,也就是出现了可见性和有序性问题。
并发三大问题
- 可见性:两个原因,一是缓存不一致,二是指令重排序。我们按照程序顺序执行的规则去考虑多线程环境下的结果,这是我们可预期的,但是在实际的多线程运行中,某些操作并不能按照我们的设想,看见前面操作的结果。
- 有序性:重排序导致,导致原因有编译器优化,CPU优化,内存重排序(读写缓冲区)。
- 原子性:线程切换导致
单线程的as-if-serial模型
在单线程模式下,as-if-serial
模型提供给了我们一个幻境,也就是程序都是按照顺序执行的,并且前面的操作可以被后面的操作看见。
但实际上,运行过程中出于优化,指令会被重排提高效率。
他是面向结果的,只要语义相同,就可以重排优化。例如两条语句无依赖性就可以重排,而且因为无依赖,他们之间的可见性也没有什么意义,因此可见性也不管了。
但是从逻辑上看,我们还是用保证了顺序和可见性的眼光去看,这和实际的运行最终效果无差别。
因此,这种模型兼顾了对于程序员友好的特性和允许机器优化的特性。
多线程的happens-before原则
happens-before
原则是一套规范,遵循这个规范,我们可以做到和as-if-serial类似的效果。happens-before
可以看作as-if-serial
的多线程版本。
由于编译器、CPU等本身可以保证单线程的as-if-serial
,因此单线程语义维持不变的情况下可以重排,但是这些重排放到多线程下面就出问题了。
为了解决这些问题,JMM定义了happens-before
原则,满足happens-before
关系的A和B操作,也可以保证A的操作可以被B可见,且A运行顺序先于B。但是还是那句话,这只是JMM层面的保证,实际上,这也是个幻境,实际上这是不一定的,只是逻辑上我们可以这么认为,并且最终效果是符合这个逻辑的,这简化了编程的思考,让我们能够在更加简单的模型中思考程序逻辑,而不用去管底层的实现。
JMM之所以只给了逻辑保证,是因为他也要兼顾指令重排序带来的效率问题,因此,happens-before
保证的多线程语义不变的情况下,JMM允许重排。
几个常见的happens-before
原则:
- 程序顺序规则:一个线程中的每个操作,
happens-before
于该线程中的任意后续操作。(程序顺序规则我要特别说明,因为以前我会觉得这句话和happens-before
定义中"某些情况允许重排"冲突。实际上,happens-before
给你的保证只是逻辑保证,你按照这个方式去思考,结果肯定不会错就行了,底层实现绕了啥大弯您就甭管了) - 监视器锁规则:对一个锁的解锁,
happens-before
于随后对这个锁的加锁。 - volatile 变量规则:对一个 volatile 域的写,
happens-before
于任意后续对这个 volatile 域的读。 - 传递性:如果 A
happens-before
B,且 Bhappens-before
C,那么 Ahappens-before
C。
其实我感觉这里面这个传递性是非常容易忽视的,然而他恰恰是非常有用的。
总结
Java内存模型是定义了一套规范来约束多线程模式下,线程之间的可见性、有序性问题,他的提出就是为了简化多线程编程,免除程序员去考虑底层实现(内存屏障、强制刷主存的CPU指令)的烦恼。我们可以认为,满足了他的规范的,正确同步了的程序片段,是具备可见性和有序性的。
happens-before和as-if-serial
有些不太一样,as-if-serial
不需要我们做任何操作就可以实现,但是他仅仅针对单线程。happens-before
是多线程中的一套规范,如果有两个操作不满足happens-before
原则,我们如果不加以控制,那么这两个操作的可见性和有序性是不可以被保证的。
因此,在多线程程序中,我们要善用happens-before
原则,来使得自己的程序能够在自己的预想范围内运行。所谓预想范围,就是JMM提供给我们那套幻境(单线程程序顺序运行,监视器加解锁顺序,volatile读写…)中可能运行出的结果,这些都是可以预见的。