volatile 變量提供了線程的可見性,并不能保證線程安全性和原子性。
什麼是線程的可見性:
鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。互斥即一次隻允許一個線程持有某個特定的鎖,是以可使用該特性實作對共享資料的協調通路協定,這樣,一次就隻有一個線程能夠使用該共享資料。可見性要更加複雜一些,它必須確定釋放鎖之前對共享資料做出的更改對于随後獲得該鎖的另一個線程是可見的 -- 如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一緻的值,這将引發許多嚴重問題。
具體看volatile的語義:
volatile相當于synchronized的弱實作,也就是說
volatile實作了類似synchronized的語義,卻又沒有鎖機制。它確定對volatile字段的更新以可預見的方式告知其他的線程。
volatile包含以下語義:
(1)Java 存儲模型不會對valatile指令的操作進行重排序:這個保證對volatile變量的操作時按照指令的出現順序執行的。
(2)volatile變量不會被緩存在寄存器中(隻有擁有線程可見)或者其他對CPU不可見的地方,每次總是從主存中讀取volatile變量的結果。也就是說對于volatile變量的修改,其它線程總是可見的,并且不是使用自己線程棧内部的變量。也就是在happens-before法則中,對一個valatile變量的寫操作後,其後的任何讀操作了解可見此寫操作的結果。
盡管volatile變量的特性不錯,但是volatile并不能保證線程安全的,也就是說volatile字段的操作不是原子性的,volatile變量隻能保證可見性(一個線程修改後其它線程能夠了解看到此變化後的結果),要想保證原子性,目前為止隻能加鎖!
使用Volatile的原則:
應用volatile變量的三個原則:
(1)寫入變量不依賴此變量的值,或者隻有一個線程修改此變量
(2)變量的狀态不需要與其它變量共同參與不變限制
(3)通路變量不需要加鎖
實際上,這些條件表明,可以被寫入 volatile 變量的這些有效值獨立于任何程式的狀态,包括變量的目前狀态。
第一個條件的限制使 volatile 變量不能用作線程安全計數器。雖然增量操作(x++)看上去類似一個單獨操作,實際上它是一個由讀取-修改-寫入操作序列組成的組合操作,必須以原子方式執行,而 volatile 不能提供必須的原子特性。實作正确的操作需要使 x 的值在操作期間保持不變,而 volatile 變量無法實作這點。(然而,如果将值調整為隻從單個線程寫入,那麼可以忽略第一個條件。)
大多數程式設計情形都會與這三個條件的其中之一沖突,使得 volatile 變量不能像 synchronized 那樣普遍适用于實作線程安全。清單 1 顯示了一個非線程安全的數值範圍類。它包含了一個不變式 -- 下界總是小于或等于上界。
正确使用volatile:
模式 #1:狀态标志
也許實作 volatile 變量的規範使用僅僅是使用一個布爾狀态标志,用于訓示發生了一個重要的一次性事件,例如完成初始化或請求停機。
很多應用程式包含了一種控制結構,形式為 "在還沒有準備好停止程式時再執行一些工作",如清單 2 所示:
清單 2. 将 volatile 變量作為狀态标志使用
volatile boolean shutdownRequested;
…
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
很可能會從循環外部調用 shutdown() 方法 -- 即在另一個線程中 -- 是以,需要執行某種同步來確定正确實作 shutdownRequested變量的可見性。(可能會從 JMX 偵聽程式、GUI 事件線程中的操作偵聽程式、通過 RMI 、通過一個 Web 服務等調用)。然而,使用synchronized 塊編寫循環要比使用清單 2 所示的 volatile 狀态标志編寫麻煩很多。由于 volatile 簡化了編碼,并且狀态标志并不依賴于程式内任何其他狀态,是以此處非常适合使用 volatile.
這種類型的狀态标記的一個公共特性是:通常隻有一種狀态轉換;shutdownRequested 标志從 false 轉換為 true,然後程式停止。這種模式可以擴充到來回轉換的狀态标志,但是隻有在轉換周期不被察覺的情況下才能擴充(從 false 到 true,再轉換到 false)。此外,還需要某些原子狀态轉換機制,例如原子變量。
模式 #2:一次性安全釋出(one-time safe publication)
缺乏同步會導緻無法實作可見性,這使得确定何時寫入對象引用而不是原語值變得更加困難。在缺乏同步的情況下,可能會遇到某個對象引用的更新值(由另一個線程寫入)和該對象狀态的舊值同時存在。(這就是造成着名的雙重檢查鎖定(double-checked-locking)問題的根源,其中對象引用在沒有同步的情況下進行讀操作,産生的問題是您可能會看到一個更新的引用,但是仍然會通過該引用看到不完全構造的對象)。
實作安全釋出對象的一種技術就是将對象引用定義為 volatile 類型。清單 3 展示了一個示例,其中背景線程在啟動階段從資料庫加載一些資料。其他代碼在能夠利用這些資料時,在使用之前将檢查這些資料是否曾經釋出過