JUC
Volatile 面试题

volatile 可见性

volatile关键字保证可见性:对一个被volatile关键字修饰的变量
1写操作的话,这个变量的最新值会立即刷新回到主内存中
2读操作的话,总是能够读取到这个变量的最新值,也就是这个变量最后被修改的值
3当某个线程收到通知,去读取volatile修饰的变量的值的时候,线程私有工作内存的数据失效,需要重新回到主内存中去读取最新的数据。

volatile 没有原子性

volatile 禁止重排

凭什么我们 Java 写了一个 volatile 关键字,系统底层加入内存屏障?两者的关系如何建立?

VolatileSeeDemo.java
public class VolatileSeeDemo {
    volatile boolean flag;
 
    public static void main(String[] args) {
        System.out.println("Hello, Volatile!");
    }
}

javap -v VolatileSeeDemo.class.class 文件进行反编译

terminal
...
volatile boolean flag;
  descriptor: Z
  flags: (0x0040) ACC_VOLATILE
...

我们可以看到在 Class 内的 Field 的 flags 上增加了 ACC_VOLATILE

JVM在把字节码转换成机器码的时候,发现标记为 volatile 的变量,会按照 JMM 的规范,在相应的位置插入内存屏障。

内存屏障是什么?

是一种屏障指令,它使得CPU或编译器对屏障指令的所发出的内存操作执行一个排序的约束。也称为内存栅栏或栅栏指令。

内存屏障能干吗?

  • 阻止屏障两边的指令重排序
  • 写操作时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
  • 读操作时加入屏障,线程私有工作内存的数据失效,重新回到主物理内存中获取最新值

内存屏障四大指令

  • 在每一个volatile写操作前面插入一个StoreStore屏障

  • 在每一个volatile写操作后面插入一个StoreLoad屏障
  • 在每一个volatile读操作后面插入一个LoadLoad屏障

  • 在每一个volatile读操作后面插入一个LoadStore屏障

对比 java.util.concurrent.locks.Lock 理解

cpu 执行机器码指令的时候,是使用 lock 前缀指令来实现 volatile 的功能的。

Lock 指令,相当于内存屏障,功能也类似内存屏障的功能:

  • 首先对总线/缓存加锁,然后去执行后面的指令,最后释放锁,同时把高速缓存的数据刷新回到主内存。
  • 在 lock 锁住总线/缓存的时候,其他 cpu 的读写请求就会被阻塞,直到锁释放。Lock 过后的写操作,会让其他 cpu 的高速缓存中相应的数据失效,这样后续这些 cpu 在读取数据的时候,就会从主内存去加载最新的数据

加了 Lock 指令过后的具体表现就和 JMM 添加内存屏障后一样。

总结

  • volatile写之前的操作,都禁止重排序到volatile之后
  • volatile读之后的操作,都禁止重排序到volatile之前
  • volatile写之后volatile读,禁止重排序