Java 内存模型 final 和 volatile 的内存语义
前提:内存屏障
内存屏障(Memory Barrier)与内存栅栏(Memory Fence)是同一个概念。
用于阻止指令重排序。保证了特定操作的执行顺序和某些变量的内存可见性。
JMM 内存屏障分为四类:
- Store:将处理器缓存的数据刷新到内存中。
- Load:将内存存储的数据拷贝到处理器的缓存中。
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad | Load1;LoadLoad;Load2 | 该屏障确保 Load1 数据的装载先于 Load2 及其后所有装载指令的操作 |
StoreStore | Store1;StoreStore;Store2 | 该屏障确保 Store1 立刻刷新数据到内存(使其对其他处理器可见)的操作先于 Store2 及其后所有存储指令的操作 |
LoadStore | Load1;LoadStore;Store2 | 确保 Load1 的数据装载先于 Store2 及其后所有的存储指令刷新数据到内存的操作 |
StoreLoad | Store1;StoreLoad;Load2 | 该屏障确保 Store1 立刻刷新数据到内存的操作先于 Load2 及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令 |
StoreLoad Barriers 同时具备其它三个屏障的效果,是目前大多数 CPU 所支持的。但是相对其他屏障,该屏障的开销相对昂贵。
一、final
https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.5
声明一个 final 成员时,必须在构造函数退出前设置它的值。
class FinalFieldExample { final int x; public FinalFieldExample() { x = 3;
可见性
final 修饰的变量作为不可变变量,只要对象是正确构造的(没有 this 逃逸发生),不需要任何同步措施就可以保证任何线程都能读到变量在构造函数中被初始化之后的值。
关于this逃逸:
https://www.jianshu.com/p/49068f29a460
https://www.hollischuang.com/archives/2583
有序性
final 写:“构造函数内对一个 final 域的写入”,与, “随后把这个被构造对象的引用赋值给一个引用变量”,这两个操作之间不能重排序。
final 读:“初次读一个包含 final 域的对象的引用” ,与, “随后初次读对象的 final 域”,这两个操作之间不能重排序(先赋值,再调用)。
二、volatile
原子性
32 位的 JDK 中 volatile 修饰后的 long 和 double 也具有原子性。但是 volatile int i = 0;i++; 就不具备原子性,因此可以理解为对单次 volatile 变量的读写操作具有原子性,复合操作(如 i++)不具有原子性。
可见性
编译器会为 volatile 修饰的变量的读写操插入内存屏障,被 volatile 修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。
有序性
CPU 可能对输入代码进行乱序执行,比如 load->add->save 有可能被优化成 load->save->add。内存屏障同样可以来限制处理器对指令进行重排序。
https://www.hollischuang.com/archives/2673
https://www.jianshu.com/p/aa432a918db9