天天看点

Java并发!Volatile关键字应该具备哪些面试谈资?

前言:虽然这几天写并发的阅读数不是很多,但是我还是想提一句。java并发基础+设计模式真的太重要了!地基打的牢固,框架和源码的阅读我们才能更加得心应手,到我们有能力阅读源码的时候也能帮助我们提升效率。(当然对于一些大佬来说,学习不同的思维也不是坏事儿,而且也希望大佬们能指正我文章中的错误。诚恳!)。

今天我也还是重复的谈一下Volatile关键字。

happens-before规则之一:

对⼀个 volatile 域的写, happens-before 于任意后续对这个 volatile 域的读。(合集文章由具体的解释哈)

JMM 是怎样禁止重排序的呢?

内存屏障(Memory Barriers / Fences)

为了实现 volatile 的内存语义,编译器在⽣成字节码时,会在指令序列中插⼊内存屏障来禁⽌特定类型的处理器重排序

这句话有点抽象,试着想象内存屏障是一面高强墙,如果两个变量之间有这个屏障,那么他们就不能互换位置(重排序)了,变量有读(Load)有写(Store),操作有前有后,JMM 就将内存屏障插⼊策略分为4种:

  1. 在每个 volatile 写操作的前⾯插⼊⼀个 StoreStore 屏障
  2. 在每个 volatile 写操作的后⾯插⼊⼀个 StoreLoad 屏障
  3. 在每个 volatile 读操作的后⾯插⼊⼀个 LoadLoad 屏障
  4. 在每个 volatile 读操作的后⾯插⼊⼀个 LoadStore 屏障

1 和 2 ⽤图形描述以及对应表格规则就是下⾯这个样⼦了:

Java并发!Volatile关键字应该具备哪些面试谈资?

3 和 4 ⽤图形描述以及对应表格规则就是下⾯这个样⼦了:

Java并发!Volatile关键字应该具备哪些面试谈资?

⼀段程序的读写通常不会像上面两种情况这样简单,这些屏障组合起来如何使⽤呢?其实⼀点都不难,我们只需要将这些指令带入到文章开头的表格中,然后再按照程序顺序拼接指令就好了

public class VolatileBarrierExample {
 private int a;
 private volatile int v1 = 1;
 private volatile int v2 = 2;
void readAndWrite(){
 int i = v1; //第⼀个volatile读
 int j = v2; //第⼆个volatile读
 a = i + j; //普通写
 v1 = i + 1; //第⼀个volatile写
 v2 = j * 2; //第⼆个volatile写
 }
}           

将屏障指令带入到程序就是这个样子:

Java并发!Volatile关键字应该具备哪些面试谈资?

我们将上图分几个角度来看:

  • 彩色是将屏障指令带⼊到程序中⽣成的全部内容,也就是编译器⽣成的「最稳妥」的⽅案
  • 显然有很多屏障是重复多余的,右侧虚线框指向的屏障是可以被「优化」删除掉的屏障

volatile 写-读的内存语义

假定线程 A 先执⾏ writer ⽅法,随后线程 B 执⾏ reader ⽅法

public class ReorderExample {
 private int x = 0;
 private int y = 1;
 private volatile boolean flag = false;
 public void writer(){
 x = 42; //1
 y = 50; //2
 flag = true; //3
 }
 public void reader(){
 if (flag){ //4
 System.out.println("x:" + x); //5
 System.out.println("y:" + y); //6
 }
 }
}           

当线程 A 执⾏ writer方法时:

Java并发!Volatile关键字应该具备哪些面试谈资?

线程 A 将本地内存更改的变量写回到主内存中。

volatile 读的内存语义:

当读⼀个 volatile 变量时, JMM 会把该线程对应的本地内存置为⽆效。线程接下来将从主内存中读取共享变量

所以当线程 B 执⾏ reader 方法时,图形结构就变成了这个样⼦

Java并发!Volatile关键字应该具备哪些面试谈资?

线程 B 本地内存变量无效,从主内存中读取变量到本地内存中,也就得到了线程 A更改后的结果,这就是 volatile 是如何保证可见性的

如果你看过前⾯的⽂章你就不难理解上⾯的两张图了,综合起来说:

  1. 线程 A 写⼀个volatile变量, 实质上是线程 A 向接下来将要读这个 volatile 变量的某个线程发出了(其对共享变量所做修改的)消息
  2. 线程 B 读⼀个 volatile 变量,实质上是线程 B 接收了之前某个线程发出的(在写这个 volatile 变量之前对共享变量所做修改的)消息。
  3. 线程 A 写⼀个 volatile 变量, 随后线程 B 读这个 volatile 变量, 这个过程实质上是线程 A 通过主内存向线程B 发送消息。