关于后端:学会了volatile你变心了我看到了

33次阅读

共计 3324 个字符,预计需要花费 9 分钟才能阅读完成。

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

volatile 简介

个别用来润饰共享变量,保障可见性和能够禁止指令重排

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

    (然而对单次读或者写保障原子性)

可见性代码示例

以下代码倡议应用 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. 其次,咱们创立个测试类

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 = true

可见,这次主线程批改的变量被另一个线程所感知到了,保障了变量的 可见性

可见性原理剖析

那么,神奇的 volatile 底层到底做了什么呢,你的扭转,逃不过他的法眼?为什么不必他润饰变量的话,变量的扭转其余线程就看不见?

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

注:本地内存是 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, 京东工程师和你一起成长

往期精彩

  • ?线程池为什么能够复用,我是蒙圈了。。
  • 实战!xhJaver 居然用线程池优化了。。。
  • 不会吧,就是你把线程池讲的这么分明的?
  • mysql 能够靠索引,而我只能靠打工,加油,打工人!
  • 原来你是这样的 xhJaver!(走心文)
  • 京东这道面试题你会吗?

正文完
 0