Java併發(四):volatile的實現原理 Java併發(一):Java記憶體模型乾貨總結
synchronized是一個重量級的鎖,volatile
通常被比喻成輕量級的synchronized
volatile
是一個變數修飾符,只能用來修飾變數。
volatile寫:當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體。
volatile讀:當讀一個volatile變數時,JMM會把該執行緒對應的本地記憶體置為無效。執行緒接下來將從主記憶體中讀取共享變數。
volatile實現原理
1)JMM把記憶體屏障指令分為下列四類:
StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他三個屏障的效果。現代的多處理器大都支援該屏障(其他型別的屏障不一定被所有處理器支援)。執行該屏障開銷會很昂貴,因為當前處理器通常要把寫緩衝區中的資料全部重新整理到記憶體中(buffer fully flush)。
Store:資料對其他處理器可見(即:重新整理到記憶體)
Load:讓快取中的資料失效,重新從主記憶體載入資料
2)JMM針對編譯器制定的volatile重排序規則表
是否能重排序 | 第二個操作 | ||
第一個操作 | 普通讀/寫 | volatile讀 | volatile寫 |
普通讀/寫 | NO | ||
volatile讀 | NO | NO | NO |
volatile寫 | NO | NO |
舉例來說,第三行最後一個單元格的意思是:在程式順序中,當第一個操作為普通變數的讀或寫時,如果第二個操作為volatile寫,則編譯器不能重排序這兩個操作。
從上表我們可以看出:
- 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。
- 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。
- 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
JMM記憶體屏障插入策略(編譯器可以根據具體情況省略不必要的屏障):
- 在每個volatile寫操作的前面插入一個StoreStore屏障。
- 對於這樣的語句Store1;
StoreStore
; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
- 對於這樣的語句Store1;
- 在每個volatile寫操作的後面插入一個StoreLoad屏障。
- 對於這樣的語句Store1;
StoreLoad
; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。
- 對於這樣的語句Store1;
- 在每個volatile讀操作的後面插入一個LoadLoad屏障。
- 對於這樣的語句Load1;
LoadLoad
; Load2,在Load2及後續讀取操作要讀取的資料被訪問前,保證Load1要讀取的資料被讀取完畢。
- 對於這樣的語句Load1;
- 在每個volatile讀操作的後面插入一個LoadStore屏障。
- 對於這樣的語句Load1;
LoadStore
; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢。
- 對於這樣的語句Load1;
volatile保證可見性
volatile修飾的變數寫之後將本地記憶體重新整理到主記憶體,保證了可見性
volatile保證有序性
volatile變數讀寫前後插入記憶體屏障以避免重排序,保證了有序性
volatile不保證原子性
volatile不是鎖,與原子性無關
要我說,由於CPU按照時間片來進行執行緒排程的,只要是包含多個步驟的操作的執行,天然就是無法保證原子性的。因為這種執行緒執行,又不像資料庫一樣可以回滾。如果一個執行緒要執行的步驟有5步,執行完3步就失去了CPU了,失去後就可能再也不會被排程,這怎麼可能保證原子性呢。
為什麼synchronized
可以保證原子性 ,因為被synchronized
修飾的程式碼片段,在進入之前加了鎖,只要他沒執行完,其他執行緒是無法獲得鎖執行這段程式碼片段的,就可以保證他內部的程式碼可以全部被執行。進而保證原子性。(摘自http://www.hollischuang.com/archives/2673)
volatile不保證原子性的例子:
/** * 建立10個執行緒,然後分別執行1000次i++操作。目的是程式輸出結果10000 * 但是,多次執行的結果都小於10000。這其實就是volatile無法滿足原子性的原因。 */ public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for (int i = 0; i < 10; i++) { new Thread() { public void run() { for (int j = 0; j < 1000; j++) test.increase(); }; }.start(); } while (Thread.activeCount() > 1) // 保證前面的執行緒都執行完 Thread.yield(); System.out.println(test.inc); } }