硬件效率与一致性
- CPU执行计算任务,需要从内存中读取和写入数据,但计算机存储设备和CPU的运算速度有几个数量级的差距。
- 在内存和CPU之间增加一层或多层高速缓存作为CPU和内存之间的缓冲(高速缓存的读写速度接近处理器)。
- 在多核处理器系统中,多个处理器任务都涉及同一块主内存区域时,可能会发生各缓存不一致的现象。
- 引入缓存一致性协议:MSE、MESI、MOSI等。
- 为了使处理器内部运算单元被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果一致(计算的最终结果是一致的,但过程结果和顺序执行的过程结果未必一致)。
java内存模型
与上一节的交互关系很类似
- 分为主内存和工作内存,主内存是虚拟机内存的一部分,每个线程有自己的工作内存。
- 所有变量存储在主内存中(变量是指可以共享的变量,比如实例字段、静态字段、构成数组对象的元素等)。
- 工作内存中保存变量副本,线程对变量的所有操作都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存。
内存间交互操作(8种)
一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存
lock(锁定):作用于主内存的变量,它把一个变量表示为一条线程独占的状态。
unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取): 作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中。
load(载入): 作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个给变量赋值的字节码指令时,执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储): 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中。
write(写入): 作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。
- 将变量从主内存拷贝到工作内存,需要顺序执行 read 和load(顺序,但不要求是连续)
- 将变量从工作内存拷贝回主内存,需要顺序执行store和write。
- 如果一个变量被assign了,就一定要同步回主存(assign->store->write)
- 如果对一个变量执行了lock,那么回清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load或assign操作以初始化该变量的值。
- 对一个变量执行unlock之前,需要将该变量同步回主内存(store->write->unlock)
原子性、可见性和有序性
原子性
- java内存模型直接保证的原子性变量操作包括:read、load、assign、use、store、write六种。可以认为,基本数据类型的访问、读写都具备原子性(除了long和double)。
- 如果需要更大范围的原子保证,使用lock和unlock(未开放给用户),但提供了字节码指令monitorenter和monitorexit来隐式使用lock和unlock。这两个字节码指令在java代码中就是synchronized关键字
可见性
当一个线程修改了共享变量的值,其他线程可以立刻得知这个修改。
- volatile:新值立刻同步回主内存,每次使用前从主内存中刷新。
- synchronized:unlock之前,要把变量同步回主内存(store->write->unlock)。
- final:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把"this"的引用传递出去,那么在其他线程中就能看见final字段的值。
有序性
- volatile:禁止指令重排
- synchronized:一个变量在同一个时刻只允许一条线程对其进行lock操作(持有同一个锁的代码块只能串行执行)。
volatile
- 可以实现可见性:
被volatile修饰的变量,修改后立刻同步回主内存。线程每次使用变量前会重新从主内存中刷新变量。(也就是平时工作内存中的变量副本可能存在缓存不一致的现象,但是每次使用前都刷新,所以执行引擎看不到不一致,因此可以认为不存在不一致的现象)。
对于volatile变量的写操作会生成lock汇编指令(相当于内存屏障),导致将缓存回写到主存中,并且使其它线程的工作内存的此变量失效。
read->load->use(load和use要连着,保证每次使用变量前都必须先从主内存中刷新最新的值,也就是保证能看见其他线程对变量进行的修改)
assign->store->write(assign和store要连着,也就是每次修改变量后,都必须立刻同步回主存) - 禁止指令重排序优化:
a,b都是被volatile修饰的变量
如果assign a 比assign b先执行,那么read a先于read b,load a先于load b
volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序和程序的顺序相同。
如果第二个操作是volatile写,不管第一个操作是什么,都不能重排序,保证volatile写之前的操作不会被编译器重排序到volatile写之后
如果第一个操作是volatile读,不管第二个操作是什么,都不能重排序,保证volatile读操作之后的操作不会被编译器重排序到volatile读之前。
插入内存屏障
在volatile写操作前面插入StoreStore
屏障,后面插入StoreLoad
屏障
- Store1;StoreStore;Store2
在Store2及后续的写入操作执行之前,保证Store1的写入操作对其他处理器可见 - Store1;StoreLoad;Load2
在Store2及后续的读取操作执行之前,保证Store1的写入操作对所有处理器可见
在volatile读操作后面插入LoadLoad
屏障,后面插入LoadStore
屏障
- Load1;LoadLoad; Load2
在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 - Load1; LoadStore; Store2
在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
不保证原子性:
所以在并发的环境下是不安全的,需要通过加锁(synchronized、juc锁或原子类)来保证原子性。
一个使用例子:DCL单例模式
public class Singleton{
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args){
Singleton.getInstance();
}
}
参考资料
《深入理解Java虚拟机》