【深入学习并发之二】volatile关键字详解

若阅读过程中出现疑问,可先阅读并发学习总览

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通过在读写语句前后加入内存屏障,实现了局部有序性。

你可能感兴趣的:(Java并发)