Java多線程(二)volatile關鍵字

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發送消息。



4 線程之間的通訊機制

4.1 通訊方式的種類

線程之間的通訊一共有兩種方式:共享內存 和 消息傳遞。

共享內存 :指的是多條線程共享同一片內存,發送者將消息寫入內存,接收者從內存中讀取消息,從而實現了消息的傳遞。但這種方式有個弊端,即須要程序員來控制線程的同步,即線程的執行次序。這種方式並無真正地實現消息傳遞,只是從結果上來看就像是將消息從一條線程傳遞到了另外一條線程。

消息傳遞: 顧名思義,消息傳遞指的是發送線程直接將消息傳遞給接收線程。因爲執行次序由併發機制完成,所以不須要程序員添加額外的同步機制,但須要聲明消息發送和接收的代碼。

Java使用共享內存的方式實現多線程之間的消息傳遞。所以,程序員須要寫額外的代碼用於線程之間的同步。

5 Java內存模型

Java 內存模型規定全部的變量都是存在 主存當中,每一個線程都有本身的 工做內存(相似於前面的高速緩存)。線程對變量的全部操做都必須在工做內存中進行,而不能直接對主存進行操做,而且每一個線程不能訪問其餘線程的工做內存。

5.1 Java內存模型通訊結構示意圖以下


Java 內存模型規定全部的變量都是存在 主存當中,每一個線程都有本身的 工做內存(相似於前面的高速緩存)。線程對變量的全部操做都必須在工做內存中進行,而不能直接對主存進行操做,而且每一個線程不能訪問其餘線程的工做內存。



從圖來看,若是線程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多線程核心編程藝術》,關於今天的更新就到這裏啦。

相關文章
相關標籤/搜索