JAVA内存模型(JMM)

Java 虚拟机规范通过来定义一个JMM来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果

JMM与Java内存结构:

对比项 JMM 内存结构
区别 抽象的,描述一组规则指明线程如何处理共享内存与私有内存 划分是具体的,是JVM运行Java程序时,必要的内存划分
联系 都存在私有数据区域和共享数据区域

硬件层数据一致性

由于计算机的存储设备与处理器的运算速度有着几个数量级的差距,所以现代计算机系统都不得不加入一层或多层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲

屏幕截图 2020-11-10 094846

这样加入一个中间缓冲区就会导致当有多个处理器时 每个处理器都有自己的高速缓存 但又共用一个缓存 会造成数据的不一致性

所以这就需要各个处理器在读写时遵守一些协议,如有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等

现代CPU的数据一致性实现 = 缓存锁(MESI ...) + 总线锁

批注 2020-07-22 144020

L1 L2缓存在核内,L3缓存核间共享

CPU可以一次读取整个缓存行,所以利用缓存行的对齐能够提高效率(disruptor)

主内存与工作内存

Java内存模型规定了所有的变量都存储在主内存, 每条线程还有自己的工作内存(本地内存),线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据,各个线程间变量的传递也必须通过主内存

批注 2020-03-12 195705

这里主内存/工作内存的划分不与堆栈等在同一层次

更多地 工作内存指的是底层CPU缓存或者寄存器 主内存指的是物理主内存

乱序问题

CPU为了提高指令执行效率,除了增加缓存之后,也会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系

硬件级防乱序

X86

sfence: store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。 lfence:load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。 mfence:modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成

原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序

JVM级防乱序(JSR133)

JVM内存屏障 屏障两边的指令不可以重排序

LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2, 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕

StoreStore屏障: 对于这样的语句Store1; StoreStore; Store2, 在Store2及后续写入操作执行前,保证Store1的写 入操作对其它处理器可见。

LoadStore屏障: 对于这样的语句Load1; LoadStore; Store2, 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2, 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

再低一层,虚拟机的实现是可以依赖于 lock 指令

volatile的细节

当一个变量被定义成volatile之后,它将具备两项特性:第一项是保证此变量对所有线程的可见性,第二个特性是禁止指令重排序优化

volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢上一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行

底层实现

ACC_VOLATILE 修饰符

volatile内存区的读写 都加屏障

StoreStoreBarrier

volatile 写操作

StoreLoadBarrier

LoadLoadBarrier

volatile 读操作

LoadStoreBarrier

内存屏障/Lock指令

synchronized实现细节

ACC_SYNCHRONIZED

monitorenter monitorexit

C C++ 调用了操作系统提供的同步机制

X86 : lock cmpxchg / xxx

compxchg [ax] (隐式参数,EAX累加器), [bx] (源操作数地址), [cx] (目标操作数地址)

第一个操作数不在指令里面出现,是一个隐式的操作数,也就是 EAX 累加寄存器里面的值。第二个操作数就是源操作数,并且指令会对比这个操作数和上面的累加寄存器里面的值

如果值是相同的,那一方面,CPU 会把 ZF(也就是条件码寄存器里面零标志位的值)设置为 1,然后再把第三个操作数(也就是目标操作数),设置到源操作数的地址上。如果不相等的话,就会把源操作数里面的值,设置到累加器寄存器里面

内存间的交互操作

202031219588

在最新的JSR133中,将这8大操作简化为read、write、lock和unlock四种

三大特性

Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的

原子性

JAVA内存模型保证了8种内存操作具有原子性

但在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行

但对于大多数情况,除非知道long 或者 double变量会有线程争用,否则没有必要使用volatile修饰它们

虚拟机提供了monitorenter monitorexit来提供更大范围原子性的保证 对应的语言层面就是synchronized关键字

可见性

可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改

变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性

实现可见性的方式:

有序性

在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序

指令重排序:Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性

volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前

synchronized 也可以来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性

as-if-serial语义

as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变

先行发生原则

Java内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在

时间先后顺序与先行发生原则之间基本没有因果关系,所以我们衡量并发安全问题的时候不要受时间顺序的干扰,一切必须以先行发生原则为准