文章目录
JMM–Java内存模型
Java内存模型是Java语言在多线程并发情况下对于共享变量读写(实际是共享变量对应的内存操作)的规范,主要是为了解决多线程可见性、原子性的问题,解决共享变量的多线程操作冲突问题。
JMM的出现,能够屏蔽掉各种硬件和操作系统的内存访问差异,实现平台一致性,使得Java程序能够一次编写,到处运行。
JMM的同步约定:
- 线程解锁前,必须把共享变量立即放回主存
- 线程加锁前,必须读取主存中的最新值到工作内存中
- 加锁和解锁是同一把锁
JMM原子操作:
原子操作 | 作用 |
---|---|
read(读取) | 从主内存读取数据 |
load(载入) | 将主内存读取到的数据写入工作内存 |
use(使用) | 从工作内存读取数据来计算 |
assign(赋值) | 将计算好的之重新赋值到工作内存中 |
store(存储) | 将工作内存数据写回到主内存 |
write(写入) | 将store过去的变量赋值给主内存中的变量 |
lock(锁定) | 将主内存变量加锁,标记为线程独占状态 |
unlock(解锁) | 将主内存变量解锁,解锁后其他线程可以锁定该变量 |
JMM对这八种指令制定了如下的规则
- 不允许read和load,store和wirte操作之一单独出现
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化(load或assign)的变量。
- 一个变量同一时间只有一个线程能对其进行lock,且对于多次loack,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有lock,就不能进行unlock,也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock之前,必须把此变量同步回主内存
如何实现线程可见
①总线加锁(性能太低):
CPU从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其它CPU没法去读或写这个数据,直到这个CPU使用完数据释放加锁之后其他CPU才能读取该数据
②MESI缓存一致性协议:
多个CPU从主内存读取同一个数据到各自的高速缓存,当其中某个CPU修改了缓存里的数据,该数据会马上同步到主内存,其它CPU通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效
扩展
CPU为何要有高速缓存
解决I\O速度和CPU运算速度之间的不匹配问题。
在CPU访问存储设备时,无论是存取数据还是存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。
-
时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。比如循环、递归、方法的反复调用等。
-
空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。比如顺序执行的代码、连续创建的两个对象、数组等。
高速缓存CPU执行计算的流程:
- 程序以及数据被加载到主内存
- 指令和数据被加载到CPU的高速缓存
- CPU执行指令,把结果写回到高速缓存
- 高速缓存中的数据写回主内存
多核CPU的多级缓存一致性协议MESI
多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。
MESI协议缓存状态
在MESI协议中,每个Cache line有4个状态,可用2个bit表示,它们分别是:
状态 | 描述 |
---|---|
M 修改(Modified) | 这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 |
E 独享,互斥(Exclusive) | 这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。 |
S 共享(Shared) | 这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中。 |
I 互斥(Invalid) | 这行数据无效 |
M(Modified)和E(Exclusive)状态的Cache line中数据是独有的,不同点在于M状态的数据是dirty的(和内存的不一致),E状态的数据是clean的(和内存的一致)。
S(Shared)状态的Cache line,数据和其他Core的Cache共享。只有clean的数据才能被多个Cache共享。
I(Invalid)表示这个Cache line无效。