👏作者简介:大家好,我是若明天不见,BAT的Java高级开发工程师,CSDN博客专家,后端领域优质创作者
📕系列专栏:多线程及高并发系列
📕其他专栏:微服务框架系列、MySQL系列、Redis系列、Leetcode算法系列、GraphQL系列
📜如果感觉博主的文章还不错的话,请👍点赞收藏关注👍支持一下博主哦??
?时间是条环形跑道,万物终将归零,亦得以圆全完美
从 Java5 开始,Java 开始使用新的内存模型 JSR-133:Java MEMORY Model AND Thread Specification,避免在并发编程下 CPU 多级缓存和指令重排这类设计可能会导致程序运行出现的问题。主要目的是为了简化多线程编程,增强程序可移植性
JMM 本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法
volatile
、synchronized
、final
和Lock
关键字happens-before
原则Java 内存模型是一种规范,定义了很多东西:
JDK 1.5 后,Java 内存模型抽象与CPU 缓存模型十分相似
下图两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3
从整体来看,这两个步骤实质上是两个线程间发送消息,而且这个通信过程必须要经过主内存
这 Java 内存区域和内存模型是完全不一样的两个东西:
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。
重排序分三种类型:
Instruction-Level Parallelism
)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题
memory barriers
)指令,通过内存屏障指令来禁止特定类型的处理器重排序为了保证内存可见性,java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障指令分为下列四类:
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Load1; LoadLoad; Load2 | 确保 Load1 数据的装载,之前于 Load2 及所有后续装载指令的装载。 |
StoreStore Barriers | Store1; StoreStore; Store2 | 确保 Store1 数据对其他处理器可见(刷新到内存),之前于 Store2 及所有后续存储指令的存储。 |
LoadStore Barriers | Load1; LoadStore; Store2 | 确保 Load1 数据装载,之前于 Store2 及所有后续的存储指令刷新到内存。 |
StoreLoad Barriers | Store1; StoreLoad; Load2 | 确保 Store1 数据对其他处理器变得可见(指刷新到内存),之前于 Load2 及所有后续装载指令的装载。 |
StoreLoad Barriers 会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。
StoreLoad Barriers 是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)
JSR-133 提出了happens-before
的概念,happens-before
的八项规则是用于定义多线程程序中操作之间的顺序关系,以确保程序的可见性和有序性。它是Java内存模型(Java MEMORY Model,JMM)的一部分
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在
happens-before
关系
happens-before
的八项规则:
as-if-serial
原则的含义是,虚拟机可以对代码进行各种优化和重排序,只要在单线程环境下,程序的执行结果与按照源代码顺序执行的结果一致,就不会违反Java内存模型
为了遵守as-if-serial
原则,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例:
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
如上图所示,A 和 C 之间存在数据依赖关系,同时 B 和 C 之间也存在数据依赖关系。因此在最终执行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,程序的结果将会被改变)。但 A 和 B 之间没有数据依赖关系,编译器和处理器可以重排序 A 和 B 之间的执行顺序。下图是该程序的两种执行顺序:
as-if-serial
原则把单线程程序保护了起来,遵守as-if-serial
语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial
语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题
提高处理器利用率,提高程序响应性,支持并发编程,支持后台任务执行,以及实现复杂的算法和数据结构,从而提高计算机系统的性能和效率,计算机引入了多线程:
CPU Cache 缓存 的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存 的是硬盘数据用于解决硬盘访问速度过慢的问题
由于这三种特性,并发编程时需要结合
synchronized
、volatile
、Lock
等功能,保证并发下的线程安全
由于CPU缓存,没有立即写入主内存中,可能会引起一个线程对共享变量的修改,另外一个线程不能够立刻看到,导致可见性问题
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。
此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.
这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值
由于CPU分时复用(线程切换),导致操作不一定是全部已执行完毕
int i = 1;
// 线程1执行
i++;
// 线程2执行
i++;
i++
实际需要三条 CPU 指令:
由于线程切换,线程1执行了第一条指令后,就切换到线程2执行,假如线程2执行了这三条指令后,再切换会线程1执行后续两条指令,将造成最后写到内存中的i值是2而不是3
由于编译器和处理器存在指令重排序,导致程序执行的顺序不一定按照代码的先后顺序执行
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,但在真正执行这段代码的时候可能会发生指令重排序(Instruction Reorder),导致会语句2在语句1前执行
参考资料: