只要说到并发编程,volatile是永远绕不开的一个点。理解了volatile,基本上也就理解了JMM。Java内存模型中的happens-before、as-if-serial等在前文介绍过,这里只介绍volatile的内存语义实现。
在JSR-133之后,volatile可以实现线程之间的通信,加强了volatile的内存语义,即禁止volatile变量与普通变量的重排序,使得volatile的写-读与锁的释放-获取有相同的语义。
对于volatile内存语义的描述,几乎都出自于
http://gee.cs.oswego.edu/dl/jmm/cookbook.html
Doug Lea大神的这篇文章。索性照着文章翻译一遍,再加上一点个人理解。
如下表格,是Doug Lea大神给出的一个表明volatile与普通变量之间是否允许重排序的说明:
是否允许重排序 | 第二个操作 | ||
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | NO | NO |
从表格里可以看出来
- volatile写之前的操作,禁止重排序到volatile之后
- volatile读之后的操作,禁止重排序到volatile之前
- volatile写后volatile读,禁止重排序
为了实现这种内存语义,编译器会在指令序列中插入内存屏障。内存屏障有四种:
- LoadLoad
load1;LoadLoad;load2
保证load1的数据加载先于load2及其后续所有load指令的数据加载
- StoreStore
store1;StoreStore;store2
保证store1写入到数据对其他处理器可见(即刷新到内存)要先于store2及其后续store指令的写入
- LoadStore
load1;LoadStore;store2
保证load1加载的数据先于store2以及其后store指令对数据的写入
- StoreLoad
store1;StoreLoad;load2
保证store1写入的数据对其他处理器可见(即刷新到内存)要先于load2及其后续load指令对数据的加载
最后的StoreLoad屏障是开销最昂贵的一种屏障,其中一部分原因是因为他需要把写缓冲区的所有数据全部刷新到内存。
对于volatile,JMM对最为保守的内存屏障插入规则如下:
- 在每个volatile写操作之前插入一个StoreStore屏障
- 在每个volatile写操作之后插入一个StoreLoad屏障
- 在每个volatile读操作之后插入一个LoadLoad屏障
- 在每个volatile读操作之后插入一个LoadStore屏障
我们每一条来看,上述四种内存屏障是否可以满足所有情况的volatile与普通变量的禁止重排序。
volatile写操作之前插入的StoreStore屏障,保证了volatile之前的普通变量/volatile变量的写操作,禁止重排序到volatile写操作之后。即普通写–volatile写禁止重排序,volatile写–volatile写禁止重排序。
volatile写操作之后的StoreLoad屏障,保证volatile写操作之后的普通变量/volatile变量的读操作,禁止重排序到volatile写操作之前。这个内存屏障也可以加载volatile读操作之前,但是一般对于volatile的用法都是多线程读,单线程写,所以相比于加载读之前,加在读之后的性能会更好。即volatile写–普通写禁止重排序,volatile写–volatile写禁止重排序。
volatile读操作之后的LoadLoad屏障,保证了volatile读操作之后的所有普通变量/volatile变量的读操作,禁止重排序到volatile写操作之前。即volatile读–普通读禁止重排序,volatile读–volatile读禁止重排序。
volatile读操作之后的LoadStore屏障,保证了volatile读操作之后的所有普通变量/volatile变量的写操作,禁止重排序到volatile写操作之前。即volatile读–普通写禁止重排序,volatile读–volatile读禁止重排序。
和上面表格对比一下,发现少了一条:普通读–volatile写禁止重排序。即为什么volatile写之前没有LoadStore屏障。
我们想想一下,volatile写之前如果加上LoadStore屏障的效果是什么?
- 普通读–volatile写禁止重排序
- volatile读–volatile写禁止重排序
对于第2点,因为volatile读之后又LoadStore屏障,就已经达到了禁止重排序的效果。
对于第一点,我们分析是否有必要。
我们思考一下,如果普通变量读操作与volatile的写操作做了重排序,是否也可以保证多线程下程序的正确性。
比如正常执行顺序为:
普通变量读
StoreStore屏障
volatile写
StoreLoad屏障
既然可以重排序,那这两个操作之间一定不存在数据依赖关系,重排序后:
StoreStore屏障
volatile写
StoreLoad屏障
普通变量读
在其后可以有的操作为普通变量的读/写,volatile变量的读/写。
如果是普通变量的读操作,那重排序后运行结果正确,即两个普通变量的读操作,不存在数据依赖与竞态条件。
如果是volatile变量的读\写操作,因为一个是普通变量读,一个是volatile的读\写,两个变量之间本身不存在数据依赖与竞态条件。
那么唯一有影响的就是,后续为普通变量写。因为普通变量读与普通变量写之间没有happens-before规则,所以会有竞态条件,但是volatile的写操作的内存语义与释放锁相同,即会刷新该线程的写缓冲到内存中,而普通变量读根本不涉及到写缓冲,所以即使重排序了也不会破坏volatile的内存语义。
所以,不需要在volatile的写操作前加LoadStore屏障。