volatile使用总结
背景
volatile的出现与java的内存模型(JMM)密不可分。
大家都知道,cpu与内存的频率差别巨大,为了提高cpu的使用率,现代计算机都设置有多级缓存(现在最高好像是3级?)。java是跨平台语言,所以jvm规范专门抽象了一层概念模型,用于屏蔽各平台差异,这层概念模型即为JMM。
JMM把内存分成2大块,工作内存和主内存。java线程只与工作内存交互,同时,JMM规定了一套交互协议用于实现工作内存和主内存的数据同步。
java内存模型和jvm内存模型不同,个人理解是,JMM解决的是数据存取问题,jvm内存模型规定了jvm如何使用内存问题。JMM的工作内存应该是用jvm的虚拟机栈实现的?
volatile语义
下面,说一下volatile能实现什么功能。
- 保证变量在多线程之间的可见性。
- 禁止指令重排序。
语义解释
保证变量在多线程之间的可见性
下边的代码体现了使用volatile关键字前后的区别。
/*
* @author https://blog.youkuaiyun.com/wjj0535
* @since JDK 1.8
*/
public class VolatileTest {
private static volatile int testInt = 0;
// private static int testInt = 0;
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
while(testInt == 0) {}
System.out.println("A Thread: return" + System.currentTimeMillis());
}
}.start();
new Thread(){
@Override
public void run() {
while(testInt == 0) {}
System.out.println("B Thread: return" + System.currentTimeMillis());
}
}.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
testInt++;
}
}
不使用volatile时,A、B线程均处于死循环状态;
使用volatile后,A、B线程会1s后同时退出,控制台打印:
B Thread: return1617774163455
A Thread: return1617774163455
可以看到,没有volatile时,线程使用的工作内存值,所以testInt==0一直成立。使用volatile后,testInt++导致while条件失效,并且两个线程在毫秒级别同时退出。
禁止指令重排序
现代cpu为了提高并行能力,一般会在不影响程序语义的情况下对程序指令重新编排,放进不同的槽位内。这会造成某些语句,如变量赋值等并没有按照程序员期望的顺序执行。用伪代码举个栗子:
boolean isDone = false;
new Thread(){
public void run(){
readConfigFileFromDisk();
isDone = true;
}
}.start();
new Thread(){
public void run(){
while(!isDone){};
doSomeThing();
}
}.start();
上述代码,有可能readConfigFileFromDisk()还没执行,doSomeThing()就已经执行了,导致逻辑错误。
注意点
volatile只会保证多线程间的变量的可见性,不会保证变量操作的原子性。
比如语句i++,分三步执行:
- 取最新数据
- 工作内存执行+1操作
- 写回工作内存并通知其他工作内存失效。
执行第二步时,其他线程会同时在执行。所以在高并发场景的原子操作,volatile不适用,还是需要锁来实现。