JMM(Java内存模型)
CPU缓存模型
CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存数据传递,解决不匹配的问题,对于多核解决了总线被占用的问题。内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。
指令重排序
系统执行代码的顺序不一定是你写的代码的顺序。
为什么要指令重排序?
计算机在执行过程中,为了提高性能,会对编译器和编译器做指令重排序。
流水线技术
为了提高利用率以及程序的性能, Java 虚拟机会在你这个指令还没完全执行完毕的时候,就去执行另外一个指令。这就是流水线技术。
流水线最怕的是啥?是我执行着命令,执行着命令,突然中断了,恢复中断的成本是很大的,所以就要想尽办法,绞尽脑汁不要让中断的情况发生。从而出现指令重排序。
我们知道计算机在执行的时候都是一个个指令去执行,不同的指令可能操作的硬件不一样,在执行的过程中可能会产生中断
,打个比方,两个指令a和b他们操作的东西各不相同,如果加载a的时候停顿了,b就加载不到,但是实际上它们互补影响,我也可以先加载b在加载a,所以指令重排
是减少停顿的一种方法,这样大大提高了效率。
Java 源代码会经历 编译器优化重排 —> 指令并行重排 —> 内存系统重排 的过程,最终才变成操作系统可执行的指令序列。
这样会导致一个问题--
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
JMM 是如何抽象线程和主内存之间的关系
Java 内存模型(JMM) 抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中。
什么是主内存?什么是本地内存?
主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量,还是局部变量,类信息、常量、静态变量都是放在主内存中。为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
本地内存:每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。每个线程只能操作自己本地内存中的变量,无法直接访问其他线程的本地内存。如果线程间需要通信,必须通过主内存来进行。本地内存是 JMM 抽象出来的一个概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
从上图来看,线程 1 与线程 2 之间如果要进行通信的话,必须要经历下面 2 个步骤:
-
线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。
-
线程 2 到主存中读取对应的共享变量的值。
也就是说,JMM 为共享变量提供了可见性的保障。
不过,多线程下,对主内存中的一个共享变量进行操作有可能诱发线程安全问题。举个例子:
-
线程 1 和线程 2 分别对同一个共享变量进行操作,一个执行修改,一个执行读取。
-
线程 2 读取到的是线程 1 修改之前的值还是修改后的值并不确定,都有可能,因为线程 1 和线程 2 都是先将共享变量从主内存拷贝到对应线程的工作内存中。
happens-before原则
如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,并且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么 JMM 也允许这样的重排序。
传递规则:如果 A happens-before B,且 B happens-before C,那么 A happens-before C;
happens-before 原则表达的意义其实并不是一个操作发生在另外一个操作的前面,虽然这从程序员的角度上来说也并无大碍。更准确地来说,它更想表达的意义是前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。
举个例子:操作 1 happens-before 操作 2,即使操作 1 和操作 2 不在同一个线程内,JMM 也会保证操作 1 的结果对操作 2 是可见的。
注--图片来自于JavaGuide。