volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制。当一个变量被声明为volatile之后,它具备三种特性。
- 保证可见性:对共享变量的修改,其他线程可以立即感知到。
- 保证有序性:编译器和runtime会禁止重排序
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性
volatile的实现原理
Java代码如下
instance = new DCLSingleton(); // instance是volatile变量
转成汇编代码如下
有volatile关键字修饰的共享变量进行写操作的时候会在指令前插入Lock前缀
,该指令会在多核处理器下引入两件事情:
- 将当前处理器缓存行的数据写回到系统内存
Lock前缀指令导致在执行指令期间,声言处理器的LOCK#
信号,如果访问的内存区域已经缓存在处理器内部,则会锁定这块区域的缓存并写会到内存,并使用缓存一致性机制来确保修改的原子性。 - 这个写会内存的操作会使其他CPU里缓存了该内存地址的数据无效
处理器使用MESI控制协议去维护内部缓存和其他处理器缓存的一致性。
volatile的内存语义
从JSR-133开始(即从JDK5开始),volatile变量的写-读可以实现线程间的通信。从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果。
volatile写的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读的内存语义
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
volatile内存语义的实现
JMM针对编译器制定的volatile重排规则表:
从上表我们可以看出:
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。
- 当第一操作是volatile读时,不管第二个操作是什么,都不能重排序。
- 当第一个操作时volatile写,第二个操作是volatile读时,不能重排序。
为了实现volatile的内存语义,编译器生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
volatile的应用场景
状态标识
public class ShutdownDemo {
private volatile boolean shutDownFlag = false;
public void doWork() {
while (!shutDownFlag) {
// do work
}
}
public void shutdown() {
shutDownFlag = true;
}
}
双重检查锁定
public class DCLSingleton {
private static volatile DCLSingleton instance = null;
private DCLSingleton() {
}
public static DCLSingleton getInstance() {
if (null == instance) {
synchronized (DCLSingleton.class) {
if (null == instance) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
需要利用顺序性
volatile通过在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
volatile与synchronized的区别
使用上的区别 | 对原子性的保证 | 对可见性的保证 | 对有序性的保证 | 其它 | |
---|---|---|---|---|---|
volatile | 修饰变量 | 不保证原子性 | 保证可见性 | 保证有序性 | 无线程阻塞 |
synchronized | 修饰方法和语句块 | 保证原子性 | 保证可见性 | 可以保证有序性,但是重量级锁会退化到串行 | 会引起线程阻塞 |
大多数场景中,volatile的总开销要比锁来的低,我们在volatile与锁(synchronized或java.util.concurrent包里面的锁)中选择的唯一性判断依据仅仅是volatile的语义能否满足使用场景的需求。
什么时候该使用volatile呢?
当且仅当满足下列所有条件时,才应该使用volatile变量:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
- 该变量不会与其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
参考资料
Java并发编程的艺术 方腾飞 魏鹏 程晓明 著
Java并发编程实战 童云兰 译