以前文章
JMM
JMM定义了共享内存中多线程程序读写操作的行为规范:在虚拟机中把共享变量存储到内存
以及从内存中取出共享变量的底层实现细节。通过这些规则来规范对内存的读写操作从而保证指令的正
确性,它解决了CPU多级缓存、处理器优化、指令重排序导致的内存访问问题,保证了并发场景下的可
见性。
volatile
如果一个字段被声明为 volatile ,java 线程内存模型确保所有线程看到这个变量的值都是一致的。可见性和有序性
被 volatile 修饰的变量进行写操作时会多出一个 Lock 前缀的指令,该指令在多核处理器下会引发两件事情
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作会使其他 CPU 里缓存了该内存地址的数据无效。
出现的问题:
当多核处理器进行写操作时,线程会将系统内存的数据加载的内部缓存中。当修改了变量的数值之后会存储在当前处理器缓存行中,对其他CPU 不可知。此时会造成其他线程访问时读取的是旧的数据造成可见性的问题。
如果对声明了 volatile 的变量进行写操作,JVM就会向处理器发送一条 Lock 前缀指令,将这个变量所在缓存行的数据写回到系统内存。就算写回到内存中,如果其他处理器缓存的值还是旧值的话,再执行计算操作就会有问题。所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议(MESI)。
MESI 缓存一致性协议
当 volatile 修饰的变量
- 只有一个CPU使用时,此时缓存行的状态是 E 独占状态
- 存在多个CPU使用时,此时缓存行的状态是 S 共享状态
- 存在多个CPU使用时且某个CPU的缓存行做了修改,此时缓存行的状态是M 修改状态,并更新其他CPU中引用了该内存地址的数据为 I 失效状态。
指令重排序
为了提高性能,编译器和处理器常常会对指令做重排序。
- 编译器优化的重排序。
- 编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。
- 如果存在数据依赖性,处理器不可以改变语句执行的顺序
- 如果不存在数据依赖性,处理器可以改变语句执行的顺序
- 内存系统的重排序。
1 属于编译器重排序,2和3 属于处理器重排序
这些重排序会导致多线程程序出现可见性的问题。为了解决这个问题,JMM 的处理器重排序规则会要求java 编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障来禁止处理器重排序。
内存屏障
LoadLoad Barriers
StoreStore Barriers
LoadStore Barriers
StoreLoad Barriers
final
编译器和处理器要遵守两个重排序规则
- 在构造函数内对一个final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
- 初次读一个包含final 域的对象的引用,与随后初次读这个final 域,这两个操作之间不能重排序。
写 final 域的重排序规则
- JMM 禁止编译器把 final 域的写重排序到构造函数之外
- 编译器会在 final 域写入之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把final 域的写重排序到构造函数之外。
当 执行一个 obj = new Object (); 时包含了两个步骤
- 构造一个 Object 类型的对象
- 把这个对象的引用赋值给引用变量 obj
写 final 域的重排序规则确保:在对象引用为任意线程可见之前,对象的final 域已经被正确初始化,而普通域不保证。
读 final 域的重排序规则
在一个线程中,初次读对象引用(即读对象)与初次读该对象包含的 final 域(即 读对象的 final 域) ,JMM 禁止处理器重排序这两个操作(这个规则仅仅针对处理器)。编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障。
读 final 域的重排序规则确保:再读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。如果该引用不为 null ,那么引用对象的 final 域一定是初始化过的。
final 域为引用类型
在构造函数内对一个final 引用的对象的成员域的写入(对象的 final 域的成员赋值),与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量(对象的引入赋值给其他引用变量),这个操作之间不能重排序。
这个不太好理解,意思是如果对象 A 的 final 引用类型的域的成员在构造函数内存在写入,不能将对象 A 的值赋值给其他引入对象
public A(){ // arr[] 是 final 域 arr[] = int[1]; // 1 arr[0]=1; // 2 } public void test{ A a = new A(); A b = a; // 3 在 arr[0] 未操作完成前,不能进行该操作。 // 正确步骤是 1 - 2 - 3 }
final 语义在处理器中的实现
写 final 域的重排序规则会要求编译器在 final 域之后,构造函数 return 之前插入一个 StoreStore 屏障。
读 final 域的重排序规则要求编译器在读 final 域的操作前面插入一个 LoadLoad 屏障。
但在 X86 处理器中,读 / 写都不会插入任何内存屏障。因为 X86 处理器不会对存在间接依赖关系的操作做重排序。