一、简介
volatile能保证可见性、有序性。
synchronize与volatile的区别就是,synchronize能保证原子性,而volatile不能。
特殊情况下可以保证原子性(比如long,64位,先读前32位再读后32位,如果这个long变量用volatile修饰就能保证原子性)。
它靠内存屏障和禁止重排序来实现可见性、有序性。
二、可见性
1.导致共享变量在线程间不可见的原因:
- 线程交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在线程工作内存和主存之间及时更新
2.可见性(synchronize、final、volatile可以保证可见性)
这篇只说volatile,synchronize在其他篇章聊。
可见性是说:一个线程对main memory的修改可以及时的被其他线程观察到。
被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
java内存模型
volatile修饰的共享变量保证可见性和有序性的原因:
1.它将当前cpu cache memory行的数据写回main memory
2.这个写回main memory的操作会使得其他CPU里缓存了该内存地址的数据无效
3.其他线程用到这个变量的时候会直接从main memory中读取,而不是使用cpu cache memory中的备份。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
三、有序性(synchronize和volatile可以保证有序性)
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般是杂乱无序的。
在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,但会影响到多线程并发执行的正确性。
java内存模型的先天有序性happens-before原则
JSR-133中定义了如下的 happens-before 规则:
- 单一线程原则:在一个线程内,程序前面的操作先于后面的操作。
- 监视器锁规则:一个unlock操作先于后面对同一个锁的lock操作发生。
- volatile变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,也就是说读取的值肯定是最新的。
- 线程启动规则:Thread对象的start()方法调用先行发生于此线程的每一个动作。
- 线程加入规则:Thread 对象的结束先行发生于 join() 方法返回。
- 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
- 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
如果两个操作执行顺序无法从happens-before原则推导出来,那么他们就不能保证有序性,jvm可以随意的对他们进行重排序。
四、内存屏障和禁止重排序
JMM四类内存屏障
屏障类型 | 指令示例 | 说明 |
LoadLoad Barriers | Load1;LoadLoad;Load2 | 确保Load1数据的装载先于Load2及所有后续装载指令 |
StoreStore Barriers | Store1;StoreStore;Store2 | 确保Store1数据刷新到内存,先于Store2及所有后续存储指令 |
LoadStore Barriers | Load1;LoadStore;Store2 | 确保Load1数据装载先Store2及所有后续存储指令 |
StoreLoad Barriers | Store1;StoreLoad;Load2 | 确保Store1数据刷新到内存,先于Load2及所有后续装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载)完成之后,才执行该屏障之后的内存访问指令。 |
JMM会针对编译器制定volatile重排序规则
是否重排序 | 第二步 | ||
第一步 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | NO | NO |
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
- 在每个volatile写操作的前面插入一个StoreStore屏障;
- 在每个volatile写操作的后面插入一个StoreLoad屏障;
- 在每个volatile读操作的后面插入一个LoadLoad屏障;
- 在每个volatile读操作的后面插入一个LoadStore屏障。
volatile写插入屏障示例:
volatile读插入屏障示例:
五、常用场景
1.状态标记量
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
因为context=loadContext()和inited=true之间的执行顺序不能保证(不符合happens-before中的任何一条),所以inited变量要用volatile修饰,以免出现这种情况:
线程1在context=loadContext()方法执行之前就先执行了inited=true,此时context根本没有加载
而,线程2恰好在此时判断出inited=true,就去执行doSomethingwithconfig(context), 此时context根本没有加载
2.双重检查
/**
* 懒汉模式 -》 双重同步锁单例模式
* 单例实例在第一次使用时进行创建
*/
public class SingletonExample4 {
// 私有构造函数
private SingletonExample4() {
}
// 1、memory = allocate() 分配对象的内存空间
// 2、ctorInstance() 初始化对象
// 3、instance = memory 设置instance指向刚分配的内存
// JVM和cpu优化,发生了指令重排
// 1、memory = allocate() 分配对象的内存空间
// 3、instance = memory 设置instance指向刚分配的内存
// 2、ctorInstance() 初始化对象
// 单例对象 volatile + 双重检测机制 -> 禁止指令重排
private volatile static SingletonExample4 instance = null;
// 静态的工厂方法
public static SingletonExample4 getInstance() {
if (instance == null) { // 双重检测机制 // B
synchronized (SingletonExample4.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample4(); // A - 3
}
}
}
return instance;
}
}