只要說到併發編程,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.htmlDoug 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屏障。