Java内存模型简单概述

引入原因

由于缓存一致性和指令重排序的存在,多线程程序的执行可能会和我们设想的结果不同,也就是出现了可见性和有序性问题。

并发三大问题

  1. 可见性:两个原因,一是缓存不一致,二是指令重排序。我们按照程序顺序执行的规则去考虑多线程环境下的结果,这是我们可预期的,但是在实际的多线程运行中,某些操作并不能按照我们的设想,看见前面操作的结果。
  2. 有序性:重排序导致,导致原因有编译器优化,CPU优化,内存重排序(读写缓冲区)。
  3. 原子性:线程切换导致

单线程的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原则:

  1. 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。(程序顺序规则我要特别说明,因为以前我会觉得这句话和happens-before定义中"某些情况允许重排"冲突。实际上,happens-before给你的保证只是逻辑保证,你按照这个方式去思考,结果肯定不会错就行了,底层实现绕了啥大弯您就甭管了)
  2. 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
  3. volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
  4. 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。

其实我感觉这里面这个传递性是非常容易忽视的,然而他恰恰是非常有用的。
在这里插入图片描述

总结

Java内存模型是定义了一套规范来约束多线程模式下,线程之间的可见性、有序性问题,他的提出就是为了简化多线程编程,免除程序员去考虑底层实现(内存屏障、强制刷主存的CPU指令)的烦恼。我们可以认为,满足了他的规范的,正确同步了的程序片段,是具备可见性和有序性的。

happens-before和as-if-serial有些不太一样,as-if-serial不需要我们做任何操作就可以实现,但是他仅仅针对单线程。happens-before是多线程中的一套规范,如果有两个操作不满足happens-before原则,我们如果不加以控制,那么这两个操作的可见性和有序性是不可以被保证的。

因此,在多线程程序中,我们要善用happens-before原则,来使得自己的程序能够在自己的预想范围内运行。所谓预想范围,就是JMM提供给我们那套幻境(单线程程序顺序运行,监视器加解锁顺序,volatile读写…)中可能运行出的结果,这些都是可以预见的。

猜你喜欢

转载自blog.csdn.net/weixin_43696693/article/details/129767164