volatile介绍
volatile概述
- volatile是
比synchronized
关键字更轻量级
的同步机制
,访问volatile变量时不
会执行加锁
操作,因此不
会使执行线程阻塞
。 - volatile保证
可见性
和禁止指令重排序
,底层是通过“内存屏障
”来实现,但不保证原子性
。 写入
volatile变量相当于退出同步代码块
,读取
volatile变量相当于进入同步代码块
。
volatile的使用场景
- 对变量的
写
入操作不依赖变量的当前值
,或能确保只有单个线程更新
变量的值; - 该变量
不会与其他状态变量一起纳入不变性条件中
;(该变量没有包含在其他变量的不变式中) - 在访问变量时
不需要加锁
。
使用案例
状态标记量
:根据状态标记,终止线程。即volatile可以在检查某个状态标记以判断是否退出循环的场景中使用。单例模式中的double check
:volatile修饰instance是因为instance = new Singleton()
这行代码不是原子性操作,给instance分配内存,调用Singleton的构造函数初始化成员变量,将instance对象指向分配的内存空间。
JMM对volatile变量定义的特殊规则:
- 当前线程每次
使用变量前都
必须先从主内存刷新最新的值
,用于保证能看到其他线程对变量的修改。(read、load、use连续执行
,从主
内存到工作
内存。) - 当前每次
修改变量后
都必须立刻同步
到主内存中,用于保证其他线程可以看到自己对线程的修改。(assign、store、write
连续执行,从工作
内存到主
内存。) - volatile修饰的变量
不会被指令重排序优化
,保证代码的执行顺序与程序的顺序相同。
可见性 && 禁止指令重排序优化原理
(通过底层的lock指令来实现)
volatile可以保证线程可见性并且提供一定的有序性,但是无法保证原子性,在JVM底层volatile是采用“内存屏障
”来实现。两层语义:保证可见性,不保证原子性
;禁止指令重排序
。
可见性
保证volatile变量对所有线程的可见性。可见性指当一条线程修改该变量值,新值对于其他线程来说是立即得知的。普通变量不可以,普通变量的值在线程间传递均需要通过主内存来完成,如线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完之后从主内存进行读取操作,新变量值才对线程B可见。
volatile底层通过lock前缀,作用使得本CPU的Cache写入内存,该写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于对Cache中的变量进行了JMM的“store和write”操作,本线程使其他线程的该变量的缓存行无效,当其他线程需要读该变量时发现该变量缓存行无效,就从主存中重新加载数据,所以保证了数据的可见性。
写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块。加锁机制既可以确保可见性又可以确保原子性,但是volatile变量只保证可见性,不保证原子性。
禁止指令重排序优化
当变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因为不会将该变量上的操作与其他内存操作一起重排序,volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方。
volatile修饰的变量会在汇编代码中多执行一个lock操作,相当于一个内存屏障(Memory Barrier,指重排序时不能把后面的指令重排序到内存屏障之前的位置)。
只有一个CPU访问内存时并不需要内存屏障;但若有多个CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性。lock指令把修改同步到内存时,意味着所有之前的操作都已经执行完毕,这样就可以形成一种给人以指令重排序无法越过内存屏障的感觉。
volatile禁止指令重排序的原则:
- 当
第二个操作是volatile写
时,不管第一个操作是什么,都不能重排序
。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。 - 当
第一个操作是volatile读
时,不管第二个操作是什么,都不能重排序
。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。 - 当
第一个操作是volatile写
,第二个操作是volatile读
时,不能重排序
。
Q&A
as-if-serial语义允许对存在控制依赖的操作做重排序的原因是什么?
在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果;但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
happen-before先行发生原则是哪些?
- 顺序执行代码规则:同一个线程中的,前面的操作 happen-before 后续的操作;
- 加解锁规则:监视器上的
解锁
操作 happen-before 其后续的加锁
操作; - volatile规则:对
volatile
变量的写
操作 happen-before 后续的读
操作; - 线程启动规则:线程的
start()
方法 happen-before 该线程所有的后续操作; - 线程终止规则:线程所有的操作 happen-before 其他线程在该线程上调用
join()
返回成功后的操作; - 线程中断规则:对线程
interrupt()
方法的调用 happen-before 被中断的代码检测到中断事件的发生,可通过Thread.interrupted()
方法检测到是否有中断发生。 - 对象终结规则:一个
对象的初始化完成
(构造函数执行结束)happen-beforefinalize()
方法的开始。 - 传递性:如果a happen-before b,b happen-before c,则a happen-before c。
加锁和volatile的区别是什么?
相同点:
写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块。
不同点:
加锁机制既可以确保可见性又可以确保原子性;但是volatile变量只保证可见性,不保证原子性。
普通变量和volatile变量的区别是什么?
volatile
的特殊规则保证新值能立即同步
到主内存
中,每次使用前
立即能够从主内存刷新
。所以volatile可以保证多线程
操作时变量的可见性
,而普通变量不保证
。
- volatile修饰变量能保证“可见性”:当一条线程修改了这个变量的值,新值对于其他线程是立即得知的。
- 普通变量不能够保证“可见性”:普通变量的值在线程间的传递均需要主内存来完成,线程A修改一个普通变量的值,先加锁从主内存读取,然后向主内存回写(lock、read、load、use、assign、store、write、unlock)。另外一条线程B在线程A回写完成后(解锁),再从主内存进行读取最新值(lock、read、load、use、unlock)操作,新变量值才会对线程B可见。
原子性的语义是什么?
volatile不保证原子性
,但是讲一下原子性的语义。
1)原子性定义:
即一个操作或者多个操作要么全部执行
且执行的过程不会
被任何因素打断
,要么都不执行
。
2)示例:
i = 2;
i = j ;
i++;
i = j + 2;
在Java中,只保证基本数据类型的变量和赋值操作是原子性操作(如果是在32位的JDK环境下,对于long和double
的64位数据
的读取不是原子
操作,会分为高低两次32位操作
。)单线程中可以将整个步骤视为原子性的,但是多线程需要通过synchronized和锁来保证原子性。
i = 2
操作是原子的
,只涉及到将基本数据类型2赋值给i;
i = j,是两个操作
,先取j值,然后将j的值赋给i,非原子操作
;
i++,是三个操作
,先取i值,然后将i值自增,最后将自增的结果赋给i,是非原子操作
;
i = j + 2,是三个操作
,先取j值,然后j+2,最后将j+2的结果赋给i,是非原子操作
;
JDK1.5之前Java无法安全使用DCL(双锁检测)来实现单例模式的原因?
volatile屏蔽指令重排序的语义在JDK1.5中才被完全修复,之前volatile变量前后的代码仍然存在重排序问题。
1)DCL单例模式:
public class Singleton {
// volatile修饰变量
private volatile static Singleton instance;
private Singleton(){}
// 单例方法
public static Singleton getInstance(){
if(instance == null){
// 加锁synchronized代码块
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton.getInstance();
}
}
2)使用静态内部类来实现更安全的机制
/* 静态内部类 */
public class SingletonInner {
// 静态内部类
private static class Holder {
private static SingletonInner singleton = new SingletonInner();
}
private SingletonInner(){}
public static SingletonInner getSingleton(){
return Holder.singleton;
}
}