【笔记】高并发编程第二阶段10讲、volatile关键字深入详解

volatitle关键字
一旦一个共享变量被volatile修饰,具备两层语义:
1.保证了不同线程的可见性
2.禁止对其重排序,也就保证了有序性
【笔记】高并发编程第二阶段10讲、volatile关键字深入详解
步骤2必须确保发生在步骤1之后。

3.并未保证原子性

【笔记】高并发编程第二阶段10讲、volatile关键字深入详解
假如 volatile INIT_VALUE = 10;
线程一:
1. read from main memory: INIT_VALUE ->10; (1)
2. INIT_VALUE= 10 + 1; (5)
3. INIT_VALUE= 11; (6)
线程二:
1. read from main memory: INIT_VALUE ->10; (2)
2. INIT_VALUE= 10 + 1; (3)
3. INIT_VALUE= 11; (4)

【笔记】高并发编程第二阶段10讲、volatile关键字深入详解

问题
在讨论原子性操作时,我们经常会听到一个说法:任意单个volatile变量的读写具有原子性,但是volatile++这种操作除外。

所以问题就是:为什么volatile++不是原子性的?

答案
因为它实际上是三个操作组成的一个符合操作。

首先获取volatile变量的值
将该变量的值加1
将该volatile变量的值写会到对应的主存地址

一个很简单的例子:

如果两个线程在volatile读阶段都拿到的是a=1,那么后续在线程对应的CPU核心上进行自增当然都得到的是a=2,最后两个写操作不管怎么保证原子性,结果最终都是a=2。每个操作本身都没啥问题,但是合在一起,从整体上看就是一个线程不安全的操作:发生了两次自增操作,然而最终结果却不是3。

分析
结合内存屏障这个概念对volatile的读写操作深入理解的话:

第一步:读
在第一步操作的指令后,会增加两个内存屏障:

在Volatile读操作后插入LoadLoad屏障,防止前面的Volatile读与后面的普通读重排序
在Volatile读操作后插入LoadStore屏障,防止前面的Volatile读与后面的普通写重排序
因此第一个指令和它后续的普通读写操作会被保证没有重排序来捣乱。通常是去内存中去读。

那么问题又来了,为什么通常去内存中读?

其实这个问题要说细的话可以很细,大概就两个关键点吧:

volatile的写操作的缓存失效机制
最后一个对volatile变量执行写操作的CPU,由于在它对应的缓存中保有最新的值,因此可以不用再去主存里面获取
具体看下面第三步的分析。

第二步:自增
这个步骤没什么特别的,就是在CPU自身的高速缓存(寄存器,L1-L3 Cache)中完成。不涉及到缓存和内存的交互。

第三步:写
volatile写算是一个重点。

根据JMM对于volatile变量类型的语义规范:volatile在编译之后,会在变量写操作时添加LOCK前缀指令。这个LOCK前缀指令在多核处理器的环境中,有这样的作用:

通知CPU将当前处理器缓存行的数据写回到系统主存中
该写回操作将使其他CPU缓存了该内存地址的数据无效
另外,内存屏障在volatile的写操作中起到了很大的作用,来保证上面两点能够实现:

在Volatile写操作前插入StoreStore屏障,防止前面其他写与本次Volatile写重排序
在Volatile写操作后插入StoreLoad屏障,防止本次的Volatile写与后面的读操作重排序