Volatile是Java虚拟机提供的轻量级同步机制
- 保证了可见性
- 不保证原子性
- 禁止指令重排
学习之前,推荐先了解一下JMM
【线程】Java内存模型JMM
什么是可见性
这里可以举个例子
图中线程A读取了主内存中的flag变量,在线程A运行的时候,线程B将主内存中的变量读取到自己的工作内存中,将true改为false,再刷新回主内存中
如果不能保证可见性,程序A会认为flag一直是true,就算实际的flag已经变成了false
而在对应的flag变量上添加volatile关键字,就可以保证可见性,也就是说只要主内存中的flag值被修改了,线程A就会第一时间知道
不保证原子性
比如执行多个线程来对一个变量进行++操作,因为++操作本身不满足原子性,所以将这个变量修改为volatile关键字也不会得到最终正确的结果
这时候我们就可以使用原子类进行操作,其原理是CAS
什么是指令重排
我们平时写的程序,计算机并不一定是按照我们写的这样去执行的
比如
int x=0; //1
int y=1; //2
x=x+1; //3
y=x+x; //4
我们预想的情况是计算机是按照1234顺序执行的
但是计算机实际上是按照2134,1324等顺序执行的,但是最终结果没有错误
但是放在多线程情况下,就可能导致错误
线程A | 线程B |
---|---|
a=4 | b=3 |
x=b | y=a |
假设a和b的初始值都是0,则按照我们的想法,两个线程是并行执行的,应该同时进行第一条语句与第二条语句,最后得出的结果应该是x=3,y=4
但是指令重排可能会导致下面两条语句先执行,上面两条语句再执行,这样在本线程内是没有错误的,但是在整个代码逻辑上就会得出x=0,y=0
的情况
还可以举一个例子
public volatile boolean testMark=false; // 共享变量
Map configOptions;
char[] configText;
//线程1 逻辑
{
//模拟读取配置信息, 当读取完成后把 testMark 设置为ture 作为通知 其他线程 配置可以读取了
configOptions=new HashMap<>();
//读取配置文件信息
configText=readConfigFile(fileName);
//把配置文件信息放入configOptions
processConfigOptions(configText,configOptions);
//标志位设为ture
testMark=true;
}
//线程2逻辑
{
//循环判断testMark 等待testMark为true
while(!testMark ){
}
//读取配置信息
doSomethingWithConfig();
}
如果不禁止 指令重排 线程1 中 把 testMark=true; 重排到了 加载配置信息前面
这个时候线程2 会立即发现 testMark=true; 并进行 配置信息的读取 但是 线程1 仅仅是先执行了testMark=true; 配置文件加载 还没执行,configOptions没有数据 这个时候你如果进行这个中操作 configOptions.get(“token”);就会报错。
参考文章
【狂神说Java】JUC并发编程最新版通俗易懂
volatile 指令重排以及为什么禁止指令重排