一般用来修饰共享变量,保证可见性和可以禁止指令重排
以下代码建议使用PC端来查看,复制黏贴直接运行,都有详细注释
我们来写个代码测试一下,多线程修改共享变量时究竟需不需要用volatile修饰变量
public class Task implements Runnable{
@Override
public void run() {
System.out.println("这是"+Thread.currentThread().getName()+"线程开始,flag是 "+Demo.flag);
//当共享变量是true时,就一直卡在这里,不输出下面那句话
// 当flag是false时,输出下面这句话
while (Demo.flag){
}
System.out.println("这是"+Thread.currentThread().getName()+"线程结束,flag是 "+Demo.flag);
}
}
2.其次,我们创建个测试类
class Demo {
//共享变量,还没用volatile修饰
public static boolean flag = true ;
public static void main(String[] args) throws InterruptedException {
System.out.println("这是"+Thread.currentThread().getName()+"线程开始,flag是 "+flag);
//开启刚才线程
new Thread(new Task()).start();
try {
//沉睡一秒,确保刚才的线程已经跑到了while循环
//要不然还没跑到while循环,主线程就将flag变为false
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
//改变共享变量flag转为false
flag = false;
System.out.println("这是"+Thread.currentThread().getName()+"线程结束,flag是 "+flag);
}
}
3.我们查看一下输出结果
image
可见,程序并没有结束,他卡在了这里,为什么卡在了这里呢,就是因为我们在主线程修改了共享变量flag为false,但是另一个线程没有感知到,这个变量的修改对另一个线程不可见
public static volatile boolean flag = true
image
可见,这次主线程修改的变量被另一个线程所感知到了,保证了变量的可见性
那么,神奇的 volatile 底层到底做了什么呢,你的改变,逃不过他的法眼?为什么不用他修饰变量的话,变量的改变其他线程就看不见?
回答此问题的时候首先,我们需要了解一下JMM(Java内存模型)
image
注:本地内存是JMM的一种抽象,并不是真实存在的,本地内存它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化之后的一个数据存放位置
为了提高程序运行效率,编译器和cpu会对代码执行的顺序进行重排列,可这有时候会带来很多问题
我们来看下代码
//指令重排测试
public class Demo2 {
private Integer number = 10;
private boolean flag = false;
private Integer result = 0;
public void write(){
this.flag = true; // L1
this.number = 20; // L2
}
public void reader(){
while (this.flag){ // L3
this.result = this.number + 1; // L4
}
}
}
假如说我们有A、B两个线程 他们分别执行write()方法和 reader()方法,执行的顺序有可能如下图所示
image
这个时候,我们就可以用volatile关键字来解决这个问题,很简单,只需
private volatile Integer number = 10;
A线程在修改number变量为20的时候,就确保这句代码的前面的代码一定在此行代码之前执行,在number处插入了内存屏障 ,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排
内存屏障又是什么呢?一共有四种内存屏障类型,他们分别是
那么volatile和这四种内存屏障又有什么关系呢,具体是怎么插入的呢?
官方提供的表格是这样的
image
我们此时回过头来在看我们的那个程序
this.flag = true; // L1
this.number = 20; // L2
由于number被volatile修饰了,L2这句话是volatile写,那么加入屏障后就应该是这个样子
this.flag = true; // L1
// StoreStore 确保flag数据对其他处理器可见(刷新到内存)先于number及所有后续存储指令的存储
this.number = 20; // L2
// StoreLoad 确保number数据对其他处理器可见(刷新到内存)先于所有后续存储指令的装载
所以L1,L2的执行顺序不被重排序