volatile的作用:
-
保证线程可见性
MESI缓存一致性协议 -
禁止指令重排序(CPU)
DCL(Double Check Lock 双重检查锁)单例;
底层由内存屏障保证。
1. 保证线程可见性
java中所有线程共享堆内存,除了共享内存外,每个线程都有自己的专属区域,都有自己的工作内存。如果在共享内存中有一个值的话,当某个线程访问这个值时会copy一份到自己的工作空间中,先在自己空间中改变后再更新堆内存中的值。在这过程中其他线程可能读到未更新的值。
在这个线程里发生的改变,不能及时反映到另外一个线程里,这就是线程之间的不可见,对这个变量值加了volatile后就能保证一个线程的改变,另外一个线程马上就能看到。CPU底层是使用MESI高速缓存一致性协议实现的。
volatile只能让被它修饰的变量具有可见性,但不能保证具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
2. 禁止指令重排序
指令重排:参考该篇CPU乱序执行一节
volatile 底层是使用内存屏障实现的,在写操作和读操作前后都加了屏障。(JVM要求这样实现,是一种规范,底层用Lock指令实现)
JVM层面上:
StoreStoreBarrier
volatile 写操作(Store)
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作(Load)
LoadStoreBarrier
DCL单例
参考懒汉式单例 写法3:Singleton单例模式 代码在下方贴出
在这个写法中是需要加volatile的,因为要防止指令重排。
第一个线程INSTANCE = new Mgr04() 经过编译器编译后的指令分为3步:
(1)给指令申请内存
(2)初始化成员变量
(3)把这块内存的内容赋值给INSTANCE。
指令重排后可能还没初始化就已经执行了(3),这时第二个线程过来判断INSTANCE不为null就会直接return INSTANCE,初始化错误。
加了volatile后防止了指令重排,一定能保证INSTANCE按顺序正常初始化。
ps:不加volatile实际上也很难碰到这种问题,但为了完全正确是应该加上的。
public class Mgr04 {
private static volatile Mgr04 INSTANCE;
// 构造方法私有化
private Mgr04() {};
// 该方法被调用时才创建实例
public static Mgr04 getInstance() {
if ( INSTANCE == null) {
// 双重检查 如果是单次检查,仍有可能有多个线程进入if判断
synchronized (Mgr04.class) {
if ( INSTANCE == null ) {
INSTANCE = new Mgr04();
}
}
}
return INSTANCE;
}
// 其它方法
public void m(){};
}
总结
volatile保证线程的可见性,同时防止指令重排序。
线程可见性在CPU级别是用MESI缓存一致性协议来保证的。
禁止指令重排序CPU级别是禁止不了的,那是CPU内部运行过程能提高效率。
但是在虚拟机级别加volatile后可以禁止指令重排,它内部是加了读屏障和写屏障,这是CPU的原语。
网友评论