volatile
前言
复习volatile实现原理之前先复习下JMM
CPU缓存和主内存的关系模型:
CPU为了解决其运算速度和内存读写速度不匹配的矛盾,CPU运算速度比内存读写快超级多
解决方法:高速缓存
cpu附近有L1,L2,L3三级缓存,之后就是主内存,引入了三级缓存带来的就是缓存的一致性问题,需要用到缓存一致性协议去解决缓存一致性问题,如MESI
缓存行
在总结MESI缓存一致性协议前先看看缓存行的定义:
- 缓存是分段的,一个段代表一个存储空间,即缓存行,也是CPU缓存中可分配的最小单位。
MESI
在MESI缓存一致性协议中定义了四种状态:
- M,modified(已修改),缓存跟主存数据不一致,如果别的CPU要读主存这一块数据,该缓存需要先回写到主存且状态变为S(Share可共享)
- E,exclusive(独占),缓存和主存数据一致,别的CPU要读这块数据时,状态变为S,要修改这块数据时,状态变为M, 在其他缓存中的数据副本被标记为I(无效)
- S,share(可共享),其他CPU也有这个数据,且数据是一致的
- I,invalid(无效),当其他CPU修改了自己也有的缓存数据时,会通知自己将这块数据置为无效,下次自己要读时必须从主存读取
MESI是基于嗅探机制让其他处理器的缓存行失效的:
- 只有缓存行处于独占E或者已修改M才能去进行缓存行的写操作,也只有在这两种状态下,缓存行是独占的,即这个缓存行只有自己的这份拷贝。
- 所以,当处理器想要写缓存,必须先获取到缓存行的独占权,且必须先发送一条指令给总线(我要开始写缓存),其他处理器因为一直在嗅探总线,所以会立马感知到这条指令,从而让自身处理器对应的缓存行失效(如果有)
JMM
JMM,java内存模型其实和CPU内存模型非常像,在JMM中,每个线程都有自己的工作内存,工作内存中的数据是主存数据的拷贝,线程和线程之间的数据通信必须依赖于主存,不能直接访问其他线程的工作内存,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。
所以,通过JMM也可以看出,如果没有做同步处理,java多线程环境下也是会出现数据一致性问题,线程和线程之间的工作内存中的数据可能不一致
而volatile为多线程下的可见性问题提供了保证
一,内存语义&原理
- 通过内存屏障,禁止指令重排序,保证了有序性
- 保证了共享变量的可见性
- 不保证原子性
内存屏障:
内存屏障分为:
- 读屏障
- 让缓存中的数据失效,强制从主存中读取
- 写屏障
- 将缓存中的新值强制写回到主存,让其他线程可见
作用:
- 禁止屏障两边的指令重排序
- 强制将写缓冲区/CPU高速缓存(L1,L2)中的数据写回到主内存,或者让相应缓存中的数据失效
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。编译器选择了一个比较保守的JMM内存屏障插入策略,这样可以保证在任何处理器平台,任何程序中都能得到正确的volatile内存语义。这个策略是:(Load代表读操作,Store代表写操作)
- 在每个volatile写操作前插入一个StoreStore屏障;
- 在每个volatile写操作后插入一个StoreLoad屏障;
- 在每个volatile读操作后插入一个LoadLoad屏障;
- 在每个volatile读操作后再插入一个LoadStore屏障。
volatile与普通变量的重排序规则:
- 如果第一个操作是volatile读,那无论第二个操作是什么,都不能重排序;
- 如果第二个操作是volatile写,那无论第一个操作是什么,都不能重排序;
- 如果第一个操作是volatile写,第二个操作是volatile读,那不能重排序。
原理:
java代码经过处理得到汇编代码后:
0x0000000002931351: lock add dword ptr [rsp],0h ;*putstatic instance
发现多了一个lock前缀
lock前缀作用:
- 早期锁总线,其他CPU对缓存行的读写操作会被阻塞,直到锁释放,后期锁缓存行,且lock操作后的写操作结果会回写到主内存,并且使其他CPU该缓存行失效。其他CPU在用到该缓存行时发现缓存行失效,会从主存中读取最新值
二,用途
- 单例模式
public class Singleton {
private static Singleton instance; // 不使用volatile关键字
// 双重锁检验
public static Singleton getInstance() {
if (instance == null) { // 第7行
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 第10行
}
}
}
return instance;
}
}
主要是对象实例化的new指令不是一个原子指令,包含了内存分配,调用init,引用指向对象地址这几个过程,cpu为了程序效率可能将其重排序了,返回出去的对象引用可能没有init初始化
- 共享状态变量