Java 内存模型(Java Memory Model, JMM)的目标是解决可见性和有序性导致的并发安全 问题。通过屏蔽各种硬件和系统的内存访问差异,使 Java 在不同平台下能达到一致的内存 访问效果
物理内存模型
物理内存在并发问题下存在的一些问题以及解决方案
-
硬件处理效率 由于存储设备和 CPU 处理器的运算速率差距很大,需要使用高速缓存作为中间缓冲。先 将需要用到的数据复制到缓存中,CPU 从缓存中获取数据进行计算,然后将结果从缓存 同步到内存中
-
缓存一致性 在多处理器系统中,每个处理器都有自己的高速缓存,它们共享同一个主存,因此可能 导致缓存数据不一致问题。因此缓存和内存交换数据时要遵守一致性协议
-
代码乱序执行优化 为了让多个处理器的运算单元被充分利用,代码的执行顺序可能和输入顺序不一致,通 过优化会对运算结果进行重组,保证结果和顺序执行无异(但代码执行顺序可能被打乱)
JMM
主内存和工作内存
JMM 的主要目标是定义程序中各个变量的访问规则,它们是如何存储到内存的,又是如何被 访问的。这里的变量不包括局部变量和方法参数,因为它们是线程私有的,不存在并发安全 问题
JMM 规定所有变量都要存储在主内存中
每个线程会有自己的工作内存(本地内存),用来保存主内存中变量的副本,工作内存是一 个抽象概念,根据不同情况可能包括高速缓存、写缓冲区、寄存器等不同的物理位置
TODO 内存交互操作
并发安全特性
-
原子性:即保证一个或多个操作在执行期间不会被打断,要么不执行要么全部执行。Java 通过 synchronized 关键字设置临界区,在进入和退出临界区时分别执行 monitorenter 和 monitorexit 两条 JVM 指令
-
可见性:指当一个线程修改了一个共享变量时,其他线程可以立刻知晓。JMM 要求变量在 修改后要从工作内存同步到主内存,读取变量时要先从主内存同步变量值,以此实现可见 性
-
有序性:在多个线程间执行非同步代码时,由于指令重排序优化,所有代码都可能交叉执 行。唯一起作用的约束:对于同步方法、同步块(synchronized)和 volatile 字段的操 作维持相对有序
- volatile: 禁止这个字段指令重排序
- synchronized: 通过互斥保证只有一条线程执行
TODO Happens-Before
内存屏障
内存屏障在 Java 中用来保证有序性和可见性,其本身就是一条 CPU 指令。在原有的指令 序列中插入内存屏障(指令),禁止这条指令之前的指令和之后的指令交换次序(同一边则 允许交换),保证程序的有序性;同时屏障指令会使处理器在写入、读取值之前,将内存的 值写入高速缓存,清空无效队列,实现缓存数据的一致性,从而保证可见性
对于 Inst1; <InstBarrier>; Inst2
, 常见的 4 种屏障指令作用如下:
-
LoadLoad: 在 Inst2 及后续读取操作要读取的数据被访问前,保证 Inst1 要读取的数 据被读取完毕
-
StoreStore: 在 Inst2 及后续写入操作执行前,保证 Inst1 的写入操作对其他处理器 可见
-
LoadStore: 在 Inst2 及后续写入操作执行前,保证 Inst1 要读取的数据被读取完毕
-
StoreLoad: 在 Inst2 及后续读取操作要读取的数据被访问前,保证 Inst1 的写入操作 对其他处理器可见。这条屏障指令是开销最大的,需要 flush 写缓冲区,清空无效队列