若阅读过程中出现疑问,可先阅读并发学习总览
volatile关键字
一、volatile满足了哪几种特性 有什么局限性
volatile满足了并发中的原子性、可见性和局部有序性,但是其中的原子性是存在局限性的。
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的可见性是最好理解的,就是通过线程间的共享内存——主存,来实现线程间的通信的。
- volatile变量的读写还有另一层语义:
在volatile变量写入主存中时,会将所有本工作线程中的共享变量一并写入主存;
在volatile变量读取时,会将本工作线程中的共享变量清空,并从主存中更新它们的值。
普通共享变量如count的读写,会只在工作内存中读取;
同时,volatile变量会直接将所有(本线程的)工作内存中的共享变量,更新到主存中。
如上所述,volatile的读写都会直接作用于主存,这也就是说,volatile变量相当于是在使用主存的某个虚拟线程里进行读写,在同一个线程间自然可以保证可见性。
四、volatile局部有序性是怎么实现的
在开始解释volatile是怎么实现有序性前,先做一些设定:
//假设在主存中有这样的变量
//其中,flag是会被多个线程访问到的volatile变量;
// count是会被多个线程访问到的共享变量,但并不是volatile的
volatile int flag;
int count;
局部有序性,即对volatile读写操作前后的读写操作进行一些约束。
- 实现方式:通过内存屏障来禁止代码重排序,维护happens-before原则
内存屏障:StoreStore、StoreLoad、LoadStore及LoadLoad屏障
StoreStore屏障的前一句写操作,和其后一句写操作不可交换位置;
StoreLoad屏障的前一句写操作,和其后一句读操作不可交换位置;
LoadStore屏障的前一句读操作,和其后一句写操作不可交换位置;
LoadLoad屏障的前一句读操作,和其后一句操作不可交换位置;
下面具体分析一下哪些语句需要加入哪些屏障:
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通过在读写语句前后加入内存屏障,实现了局部有序性。