volatile的内存语义

volatile的特性

  • volatile修饰的变量可以禁止指令重排序和保证了内存可见性和单一操作的原子性,类似i++这样的复合操作的原子性保证不了
  • volatile关键字修饰的共享变量进行写操作数,会多出一个lock前缀指令。lock前缀指令其实就相当于一个内存屏障。在多处理器下,会将当前处理器工作内存的数据回写到主内存中,并且这个回写操作会其它线程中缓存该内存地址的数据无效。相当于会在写操作后,发出一个信号给缓存了这个数的线程,告诉它们值更新了,需要从主内存中从新获取
    • JVM底层volatile是采用“内存屏障”来实现的。
  • volatile经常用于两个两个场景:状态标记两、单列模式中的DCL

volatile写-读建立的happens-before关系

1
2
3
4
5
6
7
8
9
10
11
12
13
private  int  count;  //普通变量
private volatile boolean falg; //volatile 修饰的变量
//写操作
public void writer(){
count=1; // 1
falg=true; //2
}
// 读操作
public void reader(){
if(falg){ //3
int sum=count+1; // 4
}
}
  • 假设有两个线程:线程A调用读方法, 线程B调用写方法
    根据happens-before规则,这个过程的建立分为三类:
  1. 程序次序规则: 1 happens-before 2,3 happens-before 4
  2. volatile规则:2 happens-before 3 。对一个volatile变量的写操作先行发生于后面对这个变量的读操作
  3. 传递规则: 1 happens-before 4 ;

  • 如果falg不是volatile修饰的,那么操作1操作2之间没有数据依赖性,处理器可能会对这两个操作进行重排序,这时线程A正好执行先执行了操作2,然后这时线程B抢先执行了操作3, 发现为true就执行if语句里的代码, 得到值可能就是1,而不是我们所预想的输出sum=2

volatile写-读的内存语义

  • volatile写操作:当对一个volatile共享变量写操作时,JMM会当前线程对应的更新的后的本地内存中的值强制刷新到主内存中
  • volatile读操作:当读一个volatile共享变量时,JMM会把当前线程对应的本地内存标记为无效,然后线程会从主内存中加载最新的值到工作内存中进行操作。
  • 线程A写一个volatile变量,其实就是新城A向接下来要读取这个共享变量的某个线程,发送了一个信号,告诉它我已经修改了共享变量,你的工作内存的值要被标记无效。
  • 线程B读一个volatile变量,其实就是接收了之前线程A发出的修改共享变量的信号。
  • 对一个volatile变量的写操作,随后对这个变量的读操作,其实就是两个线程之间的进行了通讯。

volatile的内存语义的实现

  • 重排序分为编译器重排序和处理器重排序,为了实现volatile内存语义,JMM会分别限制这两种重排序的内型。

    volatilec重排序规则

第一个操作 第二个操作
普通读/写 普通读/写: yes , volatile读 :yes, volatile写 :no,
volatile读 普通读/写: no , volatile读 :no, volatile写 :no,
volatile写 普通读/写: yes , volatile读 :no, volatile写 :no,
  • 当第一个操作为普通变量的读/写时,如果第二个操作是volatile写,则编译器不能重排序这个两个操作。
  • 当第一个操作是volatile读时,第二个操作不管是什么都不能重排序,这个规则确保volatile读之后的操作不会排序的它之前。
  • 当一个操作是volatile写时,第二个操作时volatile读时,不能重排序

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

  • 在每个volatile写之前插入一个StoreStore屏障
  • 在每个volatile写操作的后面插入一个StoreLoad屏障
  • 在每个volatile读操作的后面插入一个LoadLoad屏障
  • 在每个volatile读操作的后面插入一个LoadStore屏障
    volatile写指令序列示意图

volatile读指令序列示意图

觉得本文不错的话,分享一下给小伙伴吧~