volatile的底层实现原理
volatile 能保证可见性
和有序性
,但是不保证原子性
volatile为什么不保证原子性
让一个 volatile 的integer自增(i++),其实要分成3步:1) 读取volatile变量值到local; 2) 增加变量的值;3) 把local的值写回,让其它的线程可见。这3步的jvm指令为:
mov 0xc(%r10),%r8d ; Load
inc %r8d ; Increment
mov %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier
注意最后一步是内存屏障。
如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的 非原子操作,就不会保证这个变量的原子性了。
volatile如何保证有序性
volatile 底层使用内存屏障
来保证了有序性,内存屏障
其实就是一个CPU指令
从硬件层面可以分为2种:Load Barrier 和 Store Barrier即读屏障和写屏障。主要有两个作用:
- 阻止屏障两侧的指令重排序;
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
首先一个变量被volatile关键字修饰之后有两个作用:
- 对于写操作:对变量更改完之后,要立刻写回到主存中。
- 对于读操作:对变量读取的时候,要从主存中读,而不是缓存。
volatile如何保证可见性
可见性是使用了lock锁汇编指令实现,让副本数据主动刷新主内存数据,从而实现缓存数据一致性问题
LOCK指令根据CPU的不同,存在两种机制:
- 总线锁
- MESI协议
MESI 缓存一致性协议
MESI分别代表缓存行数据所处的四种状态(使用额外的两位(bit)表示),通过对这四种状态的切换,来达到对缓存数据进行管理的目的:
状态 | 描述 | 监听 |
---|---|---|
M 修改(Modify) | 该缓存行有效,数据被修改了,和内存中的数据不一致,数据只存在于本缓存行中 | 缓存行必须时刻监听所有试图读该缓存行相对应的内存的操作,其他缓存须在本缓存行写回内存并将状态置为E之后才能操作该缓存行对应的内存数据 |
E 独享、互斥(Exclusive) | 该缓存行有效,数据和内存中的数据一致,数据只存在于本缓存行中 | 缓存行必须监听其他缓存读主内存中该缓存行相对应的内存的操作,一旦有这种操作,该缓存行需要变成S状态 |
S 共享(Shared) | 该缓存行有效,数据和内存中的数据一致,数据同时存在于其他缓存中 | 缓存行必须监听其他缓存是该缓存行无效或者独享该缓存行的请求,并将该缓存行置为I状态 |
I 无效(Invalid) | 该缓存行数据无效 | 无 |
状态之间的转换
local read
和local write
分别代表本地CPU读写。
remote read
和remote write
分别代表其他CPU读写
为什么单例模式的双重检查锁,对象要声明volatile?
源码如下:
public class Singleton {
private static volatile instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
Singleton.instance = new Singleton();
}
}
}
return singleton;
}
}
原因如下:
- 因为在多线程访问到
第6行
时第二线程进入会阻塞,线程被唤醒时,检查instance变量是否为空需要保证可见性。 - 是为了在
第8行
防止Singleton初始化时的 指令重排序,因为初始化的步骤可以分为:
(1)分配对象空间
(2)调用构造函数初始化
(3)将对象赋值给变量
如果发生了 指令重排序,有可能会将赋值
步骤重排序到调构造函数
之前,那么在这时如果有线程第5行
检查instance变量是否为空,则为false并获得了不完整的对象时,在使用过程中就可能会存在问题,所以需要加上 volatile,保证可见性和有序性。