Java多執行緒(二)volatile關鍵字

_雲起_發表於2019-03-04

1 volatile的定義

Java語言規範第三版中對volatile定義如下:Java程式語言允許執行緒訪問共享變數,為了確保共享變數能夠被準確和一致地更新,執行緒應該取保通過排它鎖單獨獲得這個變數。Java語言提供了volatile,在某些情況下比鎖更方便。如果一個欄位被宣告成volatile,Java執行緒記憶體模型確保所有執行緒看到這個變數的值是一致的。

2 volatile 的記憶體語義

一旦一個 共享變數(類的成員變數、類的靜態成員變數)被 volatile 修飾之後,那麼就具備了兩層語義:

2.1 保證了不同執行緒對這個變數進行 讀取 時的可見性,即一個執行緒修改了某個變數的值 , 這新值對其他執行緒來說是立即可見的 。(volatile 解決了執行緒間 共享變數 的可見性問題)

2.1.1 使用 volatile 關鍵字會強制 將修改的值立即寫入主存;

2.1.2 使用 volatile 關鍵字的話,當執行緒 2 進行修改時,會導致執行緒 1 的量 工作記憶體中快取變數 stop 的快取行無效(反映到硬體層的話,就是 CPU 的 L1或者 L2 快取中對應的快取行無效)

2.1.3 由於執行緒 1 的工作記憶體中快取變數 stop 的快取行無效,所以執行緒 1再次讀取變數 stop 的值時 會去主存讀取。那麼,線上程 2 修改 stop 值時(當然這裡包括 2 個操作,修改執行緒 2 工作記憶體中的值,然後將修改後的值寫入記憶體),會使得執行緒 1 的工作記憶體中快取變數 stop 的快取行無效,然後執行緒 1 讀取時,發現自己的快取行無效,它會等待快取行對應的主存地址被更新之後,然後去對應的主存讀取最新的值。那麼執行緒 1 讀取到的就是最新的正確的值。

2.2 禁止進行指令重排序 ,阻止編譯器對程式碼的優化 。

volatile 關鍵字禁止指令重排序有兩層意思:

2.2.1 當程式執行到 volatile 變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且 結果已經對後面的操作 可見;在其後面的操作肯定還沒有進行。

2.2.2 在進行指令優化時,不能把 volatile 變數前面的語句放在其後面執行,也不能把 volatile 變數後面的語句放到其前面執行。

為了實現 volatile 的記憶體語義,加入 volatile 關鍵字時,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障,會多出一個 lock 字首指令。記憶體屏障是一組處理器指令,解決禁止指令重排序和記憶體可見性的問題。編譯器和 CPU 可以在保證輸出結果一樣的情況下對指令重排序,使效能得到優化。處理器在進行重排序時是會考慮指令之間的資料依賴性。

記憶體屏障 有 2 個作用:

1)先於這個 記憶體屏障 的 指令 必須先執行,後於這個 記憶體屏障的指令 必須後執行 。

2) 使得記憶體可見性。所以, 如果你的欄位是 volatile ,在讀指令前插入讀屏障,可以讓快取記憶體中的資料失效,重新從主記憶體載入資料。在寫指令之後插入寫屏障,能讓寫入快取的最新資料寫回到主記憶體。

lock 字首指令在多核處理器下會引發了兩件事情:

1).將當前處理器中這個變數所在快取行的資料會寫回到系統記憶體。

2)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成。

記憶體屏障可以被分為以下幾種型別:

LoadLoad 屏障:對於這樣的語句 Load1; LoadLoad; Load2,在 Load2 及後續讀取操作要讀取的資料被訪問前,保證 Load1 要讀取的資料被讀取完畢。

StoreStore 屏障:對於這樣的語句 Store1; StoreStore; Store2,在 Store2 及後續寫入操作執行前,保證 Store1 的寫入操作對其它處理器可見。

LoadStore 屏障:對於這樣的語句 Load1; LoadStore; Store2,在 Store2 及後續寫入操作被刷出前,保證 Load1 要讀取的資料被讀取完畢。

StoreLoad 屏障:對於這樣的語句 Store1; StoreLoad; Load2,在 Load2 及後續所有讀取操作執行前,保證 Store1 的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種記憶體屏障的功能。

3 volatile寫-讀的記憶體語義

3.1 執行緒A寫一個volatile變數,實質上是執行緒A向接下來將要讀這個volatile變數的某個執行緒發出了(其對共享變數所做修改的值)訊息。

3.2 執行緒B讀一個volatile變數,實質上是執行緒B接收了之前某個執行緒發出的(在寫這個volatile變數之前對共享變數所修改的值)訊息。

3.3 執行緒A寫一個volatile變數,隨後執行緒B讀取這個volatile變數,這個過程實質上是執行緒A通過主存向執行緒B傳送訊息。


Java多執行緒(二)volatile關鍵字

4 執行緒之間的通訊機制

4.1 通訊方式的種類

執行緒之間的通訊一共有兩種方式:共享記憶體 和 訊息傳遞。

共享記憶體 :指的是多條執行緒共享同一片記憶體,傳送者將訊息寫入記憶體,接收者從記憶體中讀取訊息,從而實現了訊息的傳遞。但這種方式有個弊端,即需要程式設計師來控制執行緒的同步,即執行緒的執行次序。這種方式並沒有真正地實現訊息傳遞,只是從結果上來看就像是將訊息從一條執行緒傳遞到了另一條執行緒。

訊息傳遞: 顧名思義,訊息傳遞指的是傳送執行緒直接將訊息傳遞給接收執行緒。由於執行次序由併發機制完成,因此不需要程式設計師新增額外的同步機制,但需要宣告訊息傳送和接收的程式碼。

Java使用共享記憶體的方式實現多執行緒之間的訊息傳遞。因此,程式設計師需要寫額外的程式碼用於執行緒之間的同步。

5 Java記憶體模型

Java 記憶體模型規定所有的變數都是存在 主存當中,每個執行緒都有自己的 工作記憶體(類似於前面的快取記憶體)。執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接對主存進行操作,並且每個執行緒不能訪問其他執行緒的工作記憶體。

5.1 Java記憶體模型通訊結構示意圖如下


Java 記憶體模型規定所有的變數都是存在 主存當中,每個執行緒都有自己的 工作記憶體(類似於前面的快取記憶體)。執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接對主存進行操作,並且每個執行緒不能訪問其他執行緒的工作記憶體。


Java多執行緒(二)volatile關鍵字

從圖來看,如果執行緒A與執行緒B之間要通訊的話,必須要經歷下面2個步驟。

1) 執行緒A把本地記憶體A中更新過的共享變數重新整理到主記憶體中去。

2) 執行緒B到主記憶體中去讀取執行緒A之前已經更新過的共享變數。


6 volatile一定能保證原子性嗎?

答案是否的,如果修改例項變數中的資料,比如i++;也就是i=i+1;則這樣的操作其實並不是一個原子操作。也就是非執行緒安全的。表示式i++的操作步驟分解如下:

1)從記憶體中取出i的值;

2)計算i的值;

3)將i的值寫到記憶體。

假如在第2步計算值的時候,另外一個執行緒也修改i的值,那麼這個時候就會出現髒資料。解決的辦法使用syschronized關鍵字。關於syschronized關鍵字明天繼續更新。

7 volatile 和 和 synchronized 區別 。

1) volatile 是變數修飾符,而 synchronized 則作用於程式碼塊或方法。

2) volatile 不會對變數加鎖,不會造成執行緒的阻塞;synchronized 會對變數加鎖,可能會造成執行緒的阻塞。

3) volatile 僅能實現變數的修改可見性,並不能保證原子性;而synchronized 則 可 以 保 證 變 量 的 修 改 可 見 性 和 原 子 性 。(synchronized 有兩個重要含義:它確保了一次只有一個執行緒可以執行程式碼的受保護部分(互斥),而且它確保了一個執行緒更改的資料對於其它執行緒是可見的(更改的可見性),在釋放鎖之前會將對變數的修改重新整理到主存中)。

4) volatile 標記的變數不會被編譯器優化,禁止指令重排序;synchronized 標記的變數可以被編譯器優化。

以上內容皆整理自網上,以及《Java併發程式設計的藝術》,《Java多執行緒核心程式設計藝術》,關於今天的更新就到這裡啦。


相關文章