学会了volatile,你变心了,我看到了

更多精彩文章,请关注xhJaver,京东工程师和你一块儿成长

volatile 简介

通常用来修饰共享变量,保证可见性和能够禁止指令重排java

  • 多线程操做同一个变量的时候,某一个线程修改完,其余线程能够当即看到修改的值,保证了共享变量的可见性
  • 禁止指令重排,保证了代码执行的有序性
  • 不保证原子性,例如常见的i++

    (可是对单次读或者写保证原子性)mysql

可见性代码示例

如下代码建议使用PC端来查看,复制黏贴直接运行,都有详细注释

咱们来写个代码测试一下,多线程修改共享变量时究竟需不须要用volatile修饰变量面试

  1. 首先,咱们建立一个任务类
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,可是另外一个线程没有感知到,这个变量的修改对另外一个线程不可见多线程

  • 若是要是用volatile变量修饰的话,结果就变成了下面这个样子

public static volatile boolean flag = trueide

可见,此次主线程修改的变量被另外一个线程所感知到了,保证了变量的可见性测试

可见性原理分析

那么,神奇的 volatile 底层到底作了什么呢,你的改变,逃不过他的法眼?为何不用他修饰变量的话,变量的改变其余线程就看不见?优化

回答此问题的时候首先,咱们须要了解一下JMM(Java内存模型)this

注: 本地内存是JMM的一种抽象,并非真实存在的,本地内存它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化以后的一个数据存放位置
  • 由此咱们能够分析出来,主线程修改了变量,可是其余线程不知道,有两种状况

    1. 主线程修改的变量尚未来得及刷新到主内存中,另外一个线程读取的仍是之前的变量
    2. 主线程修改的变量刷新到了主内存中,可是其余线程读取的仍是本地的副本
  • 当咱们用 volatile 关键字修饰共享变量时就能够作到如下两点

    1. 当线程修改变量时,会强制刷新到主内存中
    2. 当线程读取变量时,会强制从主内存读取变量而且刷新到工做内存中

指令重排

  • 何为指令重排?

为了提升程序运行效率,编译器和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()方法,执行的顺序有可能以下图所示

  • 问题分析: 如图可见,A线程的L2和L1的执行顺序重排序了,若是要是这样执行的话,当A执行完L2时,B开始执行L3,但是这个时候flag仍是为false,那么L4就执行不了了,因此result的值仍是初始值0,没有被改变为21,致使程序执行错误

这个时候,咱们就能够用volatile关键字来解决这个问题,很简单,只需

private volatile Integer number = 10;

  • 这个时候L1就必定在L2前面执行
A线程在修改 number变量为20的时候,就确保这句代码的前面的代码必定在此行代码以前执行,在 number处插入了 内存屏障 ,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排

内存屏障

内存屏障又是什么呢?一共有四种内存屏障类型,他们分别是

  1. LoadLoad屏障:

    • Load1 LoadLoad Load2 确保Load1的数据的装载先于Load2及全部后续装载指令的装载
  2. LoadStore屏障:

    • Load1 LoadStore Store2 确保Load1的数据的装载先于Store2及全部后续存储指令的存储
  3. StoreLoad屏障:

    • Store1 StoreLoad Load2 确保Store1的数据对其余处理器可见(刷新到内存)先于Load2及全部后续的装载指令的装载
  4. StoreStore屏障:

    • Store1 StoreStore Store2 确保Store1数据对其余处理器可见(刷新到内存)先于Store2及全部后续存储指令的存储
> StoreLoad 是一个全能型的屏障,同时具备其余3个屏障的效果。执行该屏障的花销比较昂贵,由于处理器一般要把当前的写缓冲区的内容所有刷新到内存中(Buffer Fully Flush)
  • 装载load 就是读 int a = load1 ( load1的装载)
  • 存储store就是写 store1 = 5 ( store1的存储)

volatile与内存屏障

那么volatile和这四种内存屏障又有什么关系呢,具体是怎么插入的呢?

  1. volatile写 (先后都插入屏障)

    • 前面插入一个StoreStore屏障
    • 后面插入一个StoreLoad屏障
  2. volatile读(只在后面插入屏障)

    • 后面插入一个LoadLoad屏障
    • 后面插入一个LoadStore屏障

官方提供的表格是这样的

咱们此时回过头来在看咱们的那个程序

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,京东工程师和你一块儿成长

往期精彩

相关文章
相关标签/搜索