更多精彩文章,请关注xhJaver,京东工程师和你一块儿成长
通常用来修饰共享变量,保证可见性和能够禁止指令重排java
(可是对单次读或者写保证原子性)mysql
如下代码建议使用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.其次,咱们建立个测试类sql
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.咱们查看一下输出结果缓存
可见,程序并无结束,他卡在了这里,为何卡在了这里呢,就是由于咱们在主线程修改了共享变量flag为false,可是另外一个线程没有感知到,这个变量的修改对另外一个线程不可见多线程
public static volatile boolean flag = true
ide
可见,此次主线程修改的变量被另外一个线程所感知到了,保证了变量的可见性测试
那么,神奇的 volatile 底层到底作了什么呢,你的改变,逃不过他的法眼?为何不用他修饰变量的话,变量的改变其余线程就看不见?优化
回答此问题的时候首先,咱们须要了解一下JMM(Java内存模型)this
注: 本地内存是JMM的一种抽象,并非真实存在的,本地内存它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化以后的一个数据存放位置
由此咱们能够分析出来,主线程修改了变量,可是其余线程不知道,有两种状况
当咱们用 volatile
关键字修饰共享变量时就能够作到如下两点
为了提升程序运行效率,编译器和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()方法,执行的顺序有可能以下图所示
这个时候,咱们就能够用volatile
关键字来解决这个问题,很简单,只需
private volatile Integer number = 10;
A线程在修改number
变量为20的时候,就确保这句代码的前面的代码必定在此行代码以前执行,在number
处插入了 内存屏障 ,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排
内存屏障又是什么呢?一共有四种内存屏障类型,他们分别是
LoadLoad屏障:
LoadStore屏障:
StoreLoad屏障:
StoreStore屏障:
> StoreLoad 是一个全能型的屏障,同时具备其余3个屏障的效果。执行该屏障的花销比较昂贵,由于处理器一般要把当前的写缓冲区的内容所有刷新到内存中(Buffer Fully Flush)
那么volatile和这四种内存屏障又有什么关系呢,具体是怎么插入的呢?
volatile写 (先后都插入屏障)
volatile读(只在后面插入屏障)
官方提供的表格是这样的
咱们此时回过头来在看咱们的那个程序
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的执行顺序不被重排序
ps:总部四号楼真是愈来愈好了,奖励本身一杯奶茶
更多精彩,请关注公众号xhJaver,京东工程师和你一块儿成长