建议先看Java内存模型
作用
一个变量被volatile修饰之后即具有两层意义:
- 一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
- 禁止进行指令重排序
是否保证可见性?
可以
老规矩,看一个栗子:
//线程1
boolean canDo = false;
while(!canDo){
do();
}
//线程2
canDo = true;
基于对内存模型的了解做一下简单的分析:
线程2会先copy一份canDo的值到工作内存,修改了值后并没有立即刷新到主存,这时可能出现线程2意外被终止,而线程1看不到canDo的最新值,那么就会陷入死循环,这显然不是我们想要的结果,于是这就出现了volatile的戏份了
canDo
被volatile
修饰后会产生如下的变化:
-
canDo
的值修改后会立即刷新到主存 - 当线程2进行修改时,会导致线程1的工作内存中缓存变量
canDo
的缓存行无效 - 线程1再次读取
canDo
值时,由于缓存行无效会直接从主存读取
上述三点体现出了立即可见
是否保证原子性?
不可以
看栗子:
public volatile int vol = 0;
public void add() {
vol++;
}
public void test() {
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
add();
};
}.start();
}
}
我们期望的结果是:10*1000,但是结果往往偏小
我们做一下分析:
vol++
这种自增操作显然不满足原子性,那么就有可能出现线程1刚将vol
值读到工作内存还没执行自增操作,虽然vol
被volatile
所修饰,但主存中它的值依然是100(假设此时值为100),此时线程2已经执行自增操作,所以截止到线程2执行完成vol
的值是101,而不是我们期望的两次自增后的102
所以结论是volatile
无法保证原子性
如何保证原子性?
三种方式:
- synchronized
- Lock
- AtomicInteger
synchronized
方式:
public synchronized void add() {
vol++;
}
Lock
方式:
public void add() {
lock.lock();
try {
vol++;
} finally{
lock.unlock();
}
}
AtomicInteger
方式:
public AtomicInteger vol = new AtomicInteger(); //java 1.5中出现的原子操作类
public void add() {
vol.getAndIncrement();
}
这里对AtomicInteger
做一下解释:
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作
是否保证有序性?
答案是,不能确保有序性,但可以一定程度上保证有序性
由上文我们可以知道volatile
可以禁止指令重排,那这里就有两层含义:
- 程序执行到
volatile
的读或者写操作时,其之前的操作肯定全部完成并且结果对后面的可见,其后面的操作还没开始执行(这里的之前之后是指代码的前后顺序) - 访问
volatile
变量的语句不能前移也不能后移
结合一个栗子来理解:
x=0; //语句1
y=1;//语句2
vol=true; //语句3 vol是volatile变量
x=1; //语句4
y=0; //语句5
语句3不能移到1,2之前也不能移到4,5之后,但是1,2和4,5之间的顺序可以调换
看一个栗子来理解volatile
确保有序性的价值:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
倘若代码中inited
不是volatile
变量,那么就会存在一个问题:
指令重排后有可能语句2在1之前执行,那么doSomethingwithconfig
就有可能在context = loadContext()
之前执行,就会出现空指针
如果inited
是volatile
变量,语句1必定在2之前执行,这样就避免了上述问题
原理
volatile
是如何保证可见性和禁止指令重排的?
“观察加入
volatile
关键字和没有加入volatile
关键字时所生成的汇编代码发现,加入volatile
关键字时,会多出一个lock
前缀指令”
lock
前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 确保指令重排序时不会把其后面的指令排到内存屏障之前,也不会把前面的指令排到内存屏障的后面
- 强制将对缓存的修改操作立即写入主存
- 如果是写操作,它会导致其他CPU中对应的缓存行无效
使用场景
由上文我们可知synchronized
是可以实现volatile
所能实现的功能的,那么volatile
对比synchronized
有什么不同?
答案:volatile
性能要优于synchronized
但是无法保证操作的原子性
所以使用volatile
需要满足两个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
上述两个条件简单的说就是需要保证操作的原子性
开发中常见的使用场景:
- 上文提到过的状态表计量
- 单例的
double check
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}