volatile 可见性 防止指令重排
可见性
什么是可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
强制线程每次从主内存中读到变量,而不是从线程的私有内存中读取变量
为什么需要保证可见性
cpu内部有缓存,当需要读取数据时,会先判断缓存中是否存在,存在则直接返回,不存在则需要再去内存中获取并存到缓存中。所以,cpu优先使用缓存中的数据;如果不保证可见性,则cpu仍然会读取到旧值,再执行计算操作就会有问题。
根据程序局部性原理,按块读取,可以提高效率,缓存行大小为64字节。
怎样保证可见性
有volatile变量修饰的共享变量进行写操作的时候,会多出一行以Lock为前缀的汇编代码,这个前缀指令会在多核处理器下引发两件事情:
1.将当前处理器缓存行的数据写回到系统内存。
2.这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
详细说明:
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。
Java代码: | instance = new Singleton();//instance是volatile变量 |
---|---|
汇编代码: | 0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp); |
但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。
所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,就会重新从系统内存中把数据读到处理器缓存里。
缓存一致性协议
最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU(中央处理器,central processing unit)写数据时,如果发现操作的变量是共享变量(被多个线程访问的变量),即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
MESI分别代表缓存行的某个状态
指令重排
指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.
可参考:Java内存模型与指令重排.
指令重排遵循的原则
as-if-serial原则
as-if-serial
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。
编译器,runtime 和处理器都必须遵守as-if-serial语义,(把单线程程序保护了起来)。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。
happens-before原则
指令重排序导致的问题
在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
以DCL中volatile为例:
publicclass Singleton {
/**
* 单例对象实例
*/private volatile static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
一个对象的创建需要三步:
1)new :分配内存空间,成员变量赋默认值,即m=0;
2)invokespecial : 调用构造方法,将成员变量赋为初始值
3)astore_1 : 将t与创建的对象建立关联
指令重排序时,可将第二步、第三步的顺序重排;第一个线程执行了第一步、第三步,还没执行第二步;第二个线程在判断(t != null),就会直接拿去用,拿到的值是0不是8。如果不是基本数据类型,拿到的默认值就是null;
volatile如何实现指令重排序
利用内存屏障,有jvm级别和cpu级别。
1)jvm级别:
2)cpu级别
sfence、lfence、mfence是x86系统有原语支持,但是有的操作系统没有
所以hotspot的实现是使用lock实现
volatile中具体实现
操作系统原语look addl(L)大多数操作系统都支持,lock直接将总线锁住
addl 是对某个寄存器的值做加操作, 后面是0,表示加的是0,原值不变
因为lock必须修饰一条指令,所以修饰到加0操作上(nop空指令lock不住)