volatile关键字详讲
volatiele关键字的作用
1:保证变量的可见性
2:禁止指令重排
1保证可见性
什么叫做可见性,这里需要先说下java内存模型(JMM)
什么是JMM
JMM,全称java Memory Model。
是java虚拟机规范定义。用于屏蔽java程序在不同操作系统不同硬件对内存访问的差异。
看如下图
java虚拟机规定,线程不能对主内存的变量直接进行读写,需要先将变量拷贝一份到自己的本地内存上,在本地内存对变量进行操作,然后再利用java虚拟机规定的操作将工作内存的变量值写到主内存中。
本地内存是线程私有的(所以叫本地内存嘛),其他线程不能访问。
什么是可见性问题
就是一个线程修改了变量,其他线程没法知道变量被修改。因为线程对变量的修改需要在本地内存里进行。本地内存操作的是变量的工作副本,假如某个变量同时被拷贝进两个线程的本地内存里,线程A对变量进行修改并写回主内存。线程B没法知道变量已经被修改这件事,手上的变量副本也是之前的,不是最新的。
代码验证如下
public class VolatileTest{
int num = 0;
public static void main(String[] args) {
VolatileTest test = new VolatileTest();
System.out.println("程序开始");
new Thread(()->{
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) {}
test.num = 100;
},"1").start();
//虽然线程1修改了num的值了,但是mian线程不知道,它还是以为num=0
//所以程序会一直到while循环里出不去
while (test.num==0){
}
System.out.println("程序结束");
}
}
结果:
volatile能够保证变量的可见性
被volatile关键字修饰的变量具有可见性。即当该变量被修改时,其他线程能立马知道
代码验证如下
public class VolatileTest{
volatile int num = 0; //用volatile关键字修饰
public static void main(String[] args) {
VolatileTest test = new VolatileTest();
System.out.println("程序开始");
new Thread(()->{
//睡眠5秒
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) {}
//修改num值
test.num = 100;
},"1").start();
//num的值一开始为0,5秒后被线程1改为100,main线程立即知道num值被修改,程序继续走下去
while (test.num==0){
}
System.out.println("程序结束");
}
}
结果:
volatile为什么能够保证可见性
1)通过缓存一致性协议
什么是缓存一致性协议?
每个处理器通过嗅探跑在总线上的数据来判断自己的缓存是不是过期。当发现过期了,就将该缓存的值设为无效。当下次处理器要对这个值进行修改时,会强制从内存里将值读回缓存里。
对volatile修饰的变量,每次数据发生变化后都会被强制写回缓存里。而由于其他处理器需要遵循缓存一致性原则,都会将内存里的该值写到自己的缓存里
2)volatile写—读建立的happends-before关系
volatile变量法则:对volatile域的写入操作happends-before于每一个后续对同一域的读操作
写一个volatile变量,JMM写回主内存里。读一个volatile,线程会先把本地内存的变量设为无效,然后从主内存读取这个变量到本地内存。线程A修改volatile变量,线程B读取,两个线程通过主内存进行通信。
2禁止指令重排
什么是指令重排
CPU在执行命令的时,并不是完全按照写好的程序顺序去执行。它会有个自己内部的优化,对一些指令的执行顺序进行调整。
比如说:
int a = 10;
int b = 20;
int c = 30;
int sum = a + b + c;
在这里,cpu在执行前三句话并不一定是按照写好的顺序,它会有个自己的优化判断,重新排序执行
volatile为什么能够禁止指令重排
答案是内存屏障。volatile的内存变量在读写操作前后都加了内存屏障。
具体为
StoreStoreBarrier
volatile写操作
StoreLoadBarrier
LoadLoadBarrier
volatile读操作
LoadStoreBarrier
那这四种屏障分别什么意思呢
1:LoadLoadBarrier
场景:Load1 – LoadLoadBarrier – Load2
Load1,Load2表示两个读指令。Load2要读取的数据被访问前,要保证Load1读取数据完毕
2::StoreStoreBarrier
场景:Store1 – StoreStoreBarrier – Store2
Store1,Store2表示两个写指令。Store2写入之前,保证Store1的写操作对其他处理器可见
3:LoadStoreBarrier
场景:Load1 – LoadStoreBarrier – Store2
Store2写之前,保证Load1要读取的都读取完
4:StoreLoadBarrier
场景:Store1 – StoreLoadBarrier – Load2
Load2读之前,保证Store1的写指令对其他处理器可见