若阅读过程中出现疑问,可先阅读并发学习总览
volatile满足了并发中的原子性、可见性和局部有序性,但是其中的原子性是存在局限性的。
volatile原子性的局限:volatile变量的写和volatile的读都是有原子性的,但是由于其实现方式并不是使用的同步的思想,所以并不能独占时间片。这也导致了诸如volatile变量的自增自减操作并没有原子性。
volatile局部有序性:在底层实现中,volatile变量的读写 以及 其前后的普通变量读写操作需要满足一定的有序性。
volatile写之前的写操作,和之后的读操作不能重排序;
volatile读之后的读写操作都不能重排序。
volatile变量的读写操作都是有原子性的,这是因为JMM对volatile变量的读写操作,进行了特殊规则的规定。原子性操作的原理一般是加锁或者循环CAS,但是volatile的原子性原理,并非这两种中的任何一种。
先看一下JMM中的一些原子操作:
主存 | 工作内存 | 线程 |
---|---|---|
read | use | 读 |
write | assign | 写 |
read—>use—>读
write<—assign<—写
read:作用在主存的变量中,将主存中的变量内容更新到工作内存中
use:作用在工作内存的变量中,将工作内存中的变量内容更新到线程中
write:作用在工作内存变量中,将线程中的变量赋给工作内存中的变量
assign:作用在主存变量中,将工作内存的变量写入主存
在读任何变量时,都会在工作内存中使用use操作,将变量从工作变量中读入线程中;但是对volatile变量,必须在调用use前先调用read,先从主存中将对应volatile变量取出,传入工作内存,再将工作内存中的变量取到线程中。
同理,在写任何变量时,都会在工作内存中使用assign操作,将变量写入工作内存中;但是对volatile变量来说,必须在assign后立即在主存中write,将变量写入主存中。
这样,volatile变量的读写就是具备原子性的了。
但是由于volatile变量实现原子性的方式,是对其读写操作顺序的限制,所以在诸如volatile++这样的多字节码(机器码)操作中,并不能保证它的原子性。
volatile的可见性是最好理解的,就是通过线程间的共享内存——主存,来实现线程间的通信的。
在开始解释volatile是怎么实现有序性前,先做一些设定:
//假设在主存中有这样的变量
//其中,flag是会被多个线程访问到的volatile变量;
// count是会被多个线程访问到的共享变量,但并不是volatile的
volatile int flag;
int count;
局部有序性,即对volatile读写操作前后的读写操作进行一些约束。
下面具体分析一下哪些语句需要加入哪些屏障:
volatile写/读 | 前的屏障 | 后的屏障 |
---|---|---|
写 | StoreStore | StoreLoad |
读 | 无 | LoadStore,LoadLoad |
volatile写之前的写操作,和之后的读操作不能重排序;
volatile读之后的读写操作都不能重排序。
volatile写是将所有工作内存中的共享变量写入主存。
普通写操作是将线程内的共享变量写入工作内存,如果将volatile写前的普通写,重排序到其后,本来应该更新到主存内的共享变量,就不会更新到主存中,所以要加入StoreStore屏障。如代码段1:
//代码段1
//volatile写前的普通写,若重排序,count不能更新到主存中
count = 1; //普通写
flag = 1; //volatile写
在volatile写之后的volatile读,很好理解,本应先更新再读取,不可先读取再更新。所以要在其之后加入StoreLoad屏障。
volatile读是将所有共享变量由主存更新至工作内存。
普通读操作是将共享变量从工作内存读入线程中,如果将volatile读之后的普通读,重排序到其前,那么本该读到最新数据的共享变量,就读到了旧数据,所以应加入volatile读后的LoadLoad屏障。如代码段2:
//代码段2
//volatile读后的普通读,若重排序,会读到count的旧数据
int flagNow = flag; //volatile读
int countNow = count; //普通读
普通写操作语义见上述内容,若将volatile读后的普通写,重排序到其前,则其更新到工作内存中的共享变量会被volatile读覆盖,无法在接下来的操作中使用,所以volatile读后需要加入LoadStore屏障。如代码段3
//代码段3
//volatile读后的普通写,若重排序,最后写的普通共享变量会被覆盖
int flagNow = flag; //volatile读
count = 1; //普通写
综上所述,
volatile写前应加入StoreStore屏障,防止普通共享变量的写入不被更新;
volatile写后应加入StoreLoad屏障,防止volatile变量读写顺序出现问题;
volatile读后应加入LoadLoad屏障,防止普通共享变量读到旧数据;
volatile读后应加入LoadStore屏障,防止普通共享变量的写入被覆盖;
1 volatile满足了读写的原子性,可见性以及局部有序性。
2 volatile通过绑定写入和读取变量的“线程—工作内存”和“工作内存—主存”两段操作实现了读写的原子性。
3 volatile通过读写时,将共享变量一并读取/写入到工作内存/主存的方式,实现了可见性。
4 volatile通过在读写语句前后加入内存屏障,实现了局部有序性。