小議volatile:它的價值,必要性 and a little usability

先看一篇網文:html

volatile 和 synchronized
2011-03-11 19:43

Java 語言中的 volatile 變量能夠被看做是一種 「程度較輕的 synchronized」;與 synchronized 塊相比,volatile 變量所需的編碼較少,而且運行時開銷也較少,可是它所能實現的功能也僅是 synchronized 的一部分。本文介紹了幾種有效使用 volatile 變量的模式,並強調了幾種不適合使用 volatile 變量的情形。java

鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)互斥即一次只容許一個線程持有某個特定的鎖,所以可以使用該特性實現對共享數據的協調訪問協議,這樣,一次就只有一個線程可以使用該共享數據。可見性要更加複雜一些,它必須確保釋放鎖以前對共享數據作出的更改對於隨後得到該鎖的另外一個線程是可見的 —— 若是沒有同步機制提供的這種可見性保證,線程看到的共享變量多是修改前的值或不一致的值,這將引起許多嚴重問題。數據庫

Volatile 變量編程

Volatile 變量具備 synchronized 的可見性特性,可是不具有原子特性。這就是說線程可以自動發現 volatile 變量的最新值。Volatile 變量可用於提供線程安全,可是隻能應用於很是有限的一組用例:多個變量之間或者某個變量的當前值與修改後值之間沒有約束。所以,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具備與多個變量相關的不變式(Invariants)的類(例如 「start <=end」)。數組

出於簡易性或可伸縮性的考慮,您可能傾向於使用 volatile 變量而不是鎖。當使用 volatile 變量而非鎖時,某些習慣用法(idiom)更加易於編碼和閱讀。此外,volatile 變量不會像鎖那樣形成線程阻塞,所以也不多形成可伸縮性問題。在某些狀況下,若是讀操做遠遠大於寫操做,volatile 變量還能夠提供優於鎖的性能優點。安全

正確使用 volatile 變量的條件架構

您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:併發

對變量的寫操做不依賴於當前值。該變量沒有包含在具備其餘變量的不變式中。 【?】框架

實際上,這些條件代表,能夠被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。函數

第一個條件的限制使 volatile 變量不能用做線程安全計數器。雖然增量操做(x++)看上去相似一個單獨操做,實際上它是一個由讀取-修改-寫入操做序列組成的組合操做,必須以原子方式執行,而 volatile 不能提供必須的原子特性。實現正確的操做須要使 x 的值在操做期間保持不變,而 volatile 變量沒法實現這點。(然而,若是將值調整爲只從單個線程寫入,那麼能夠忽略第一個條件。)【一個寫入,多個觀察】

大多數編程情形都會與這兩個條件的其中之一衝突,使得 volatile 變量不能像 synchronized 那樣廣泛適用於實現線程安全。清單 1 顯示了一個非線程安全的數值範圍類。它包含了一個不變式 —— 下界老是小於或等於上界。


清單 1. 非線程安全的數值範圍類

@NotThreadSafe public class NumberRange { private int lower, upper; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; }}

這種方式限制了範圍的狀態變量,所以將 lower 和 upper 字段定義爲 volatile 類型不可以充分實現類的線程安全;從而仍然須要使用同步。不然,若是湊巧兩個線程在同一時間使用不一致的值執行 setLower  setUpper 的話,則會使範圍處於不一致的狀態。例如,若是初始狀態是 (0, 5),同一時間內,線程 A 調用 setLower(4) 而且線程 B 調用 setUpper(3),顯然這兩個操做交叉存入的值是不符合條件的,那麼兩個線程都會經過用於保護不變式的檢查,使得最後的範圍值是 (4, 3) —— 一個無效值。至於針對範圍的其餘操做,咱們須要使 setLower()  setUpper() 操做原子化 —— 而將字段定義爲 volatile 類型是沒法實現這一目的的。

性能考慮

使用 volatile 變量的主要緣由是其簡易性:在某些情形下,使用 volatile 變量要比使用相應的鎖簡單得多。使用 volatile 變量次要緣由是其性能:某些狀況下,volatile 變量同步機制的性能要優於鎖。

很難作出準確、全面的評價,例如 「X 老是比 Y 快」,尤爲是對 JVM 內在的操做而言。(例如,某些狀況下 VM 也許可以徹底刪除鎖機制,這使得咱們難以抽象地比較 volatile  synchronized 的開銷。)就是說,在目前大多數的處理器架構上,volatile 讀操做開銷很是低 —— 幾乎和非 volatile 讀操做同樣。而 volatile 寫操做的開銷要比非 volatile 寫操做多不少,由於要保證可見性須要實現內存界定(Memory Fence),即使如此,volatile 的總開銷仍然要比鎖獲取低。

volatile 操做不會像鎖同樣形成阻塞,所以,在可以安全使用 volatile 的狀況下,volatile 能夠提供一些優於鎖的可伸縮特性。若是讀操做的次數要遠遠超過寫操做,與鎖相比,volatile 變量一般可以減小同步的性能開銷。

正確使用 volatile 的模式

不少併發性專家事實上每每引導用戶遠離 volatile 變量,由於使用它們要比使用鎖更加容易出錯。然而,若是謹慎地遵循一些良好定義的模式,就可以在不少場合內安全地使用 volatile 變量。要始終牢記使用 volatile 的限制 —— 只有在狀態真正獨立於程序內其餘內容時才能使用 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 展現了一個示例,其中後臺線程在啓動階段從數據庫加載一些數據。其餘代碼在可以利用這些數據時,在使用以前將檢查這些數據是否曾經發布過。


清單 3. 將 volatile 變量用於一次性安全發佈

public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // do lots of stuff theFlooble = new Flooble(); // this is the only write to theFlooble }}public class SomeOtherClass { public void doWork() { while (true) { // do some stuff... // use the Flooble, but only if it is ready if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } }}

若是 theFlooble 引用不是 volatile 類型,doWork() 中的代碼在解除對 theFlooble 的引用時,將會獲得一個不徹底構造的 Flooble

該模式的一個必要條件是:被髮布的對象必須是線程安全的,或者是有效的不可變對象(有效不可變意味着對象的狀態在發佈以後永遠不會被修改)。volatile 類型的引用能夠確保對象的發佈形式的可見性,可是若是對象的狀態在發佈後將發生更改,那麼就須要額外的同步。

模式 #3:獨立觀察(independent observation)

安全使用 volatile 的另外一種簡單模式是:按期 「發佈」 觀察結果供程序內部使用。例如,假設有一種環境傳感器可以感受環境溫度。一個後臺線程可能會每隔幾秒讀取一次該傳感器,並更新包含當前文檔的 volatile 變量。而後,其餘線程能夠讀取這個變量,從而隨時可以看到最新的溫度值。

使用該模式的另外一種應用程序就是收集程序的統計信息。清單 4 展現了身份驗證機制如何記憶最近一次登陸的用戶的名字。將反覆使用 lastUser 引用來發布值,以供程序的其餘部分使用。


清單 4. 將 volatile 變量用於多個獨立觀察結果的發佈

public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; }}

該模式是前面模式的擴展;將某個值發佈以在程序內的其餘地方使用,可是與一次性事件的發佈不一樣,這是一系列獨立事件。這個模式要求被髮布的值是有效不可變的 —— 即值的狀態在發佈後不會更改。使用該值的代碼須要清楚該值可能隨時發生變化。

模式 #4:「volatile bean」 模式

volatile bean 模式適用於將 JavaBeans 做爲「榮譽結構」使用的框架。在 volatile bean 模式中,JavaBean 被用做一組具備 getter 和/或 setter 方法 的獨立屬性的容器。volatile bean 模式的基本原理是:不少框架爲易變數據的持有者(例如 HttpSession)提供了容器,可是放入這些容器中的對象必須是線程安全的。

在 volatile bean 模式中,JavaBean 的全部數據成員都是 volatile 類型的,而且 getter 和 setter 方法必須很是普通 —— 除了獲取或設置相應的屬性外,不能包含任何邏輯。此外,對於對象引用的數據成員,引用的對象必須是有效不可變的。(這將禁止具備數組值的屬性,由於當數組引用被聲明爲 volatile 時,只有引用而不是數組自己具備 volatile 語義)。對於任何 volatile 變量,不變式或約束都不能包含 JavaBean 屬性。清單 5 中的示例展現了遵照 volatile bean 模式的 JavaBean:


清單 5. 遵照 volatile bean 模式的 Person 對象

@ThreadSafepublic class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; }}

volatile 的高級模式

前面幾節介紹的模式涵蓋了大部分的基本用例,在這些模式中使用 volatile 很是有用而且簡單。這一節將介紹一種更加高級的模式,在該模式中,volatile 將提供性能或可伸縮性優點。

volatile 應用的的高級模式很是脆弱。所以,必須對假設的條件仔細證實,而且這些模式被嚴格地封裝了起來,由於即便很是小的更改也會損壞您的代碼!一樣,使用更高級的 volatile 用例的緣由是它可以提高性能,確保在開始應用高級模式以前,真正肯定須要實現這種性能獲益。須要對這些模式進行權衡,放棄可讀性或可維護性來換取可能的性能收益 —— 若是您不須要提高性能(或者不可以經過一個嚴格的測試程序證實您須要它),那麼這極可能是一次糟糕的交易,由於您極可能會得不償失,換來的東西要比放棄的東西價值更低。

模式 #5:開銷較低的讀-寫鎖策略

目前爲止,您應該瞭解了 volatile 的功能還不足以實現計數器。由於 ++x 其實是三種操做(讀、添加、存儲)的簡單組合,若是多個線程湊巧試圖同時對 volatile 計數器執行增量操做,那麼它的更新值有可能會丟失。

然而,若是讀操做遠遠超過寫操做,您能夠結合使用內部鎖和 volatile 變量來減小公共代碼路徑的開銷。清單 6 中顯示的線程安全的計數器使用 synchronized 確保增量操做是原子的,並使用 volatile 保證當前結果的可見性。若是更新不頻繁的話,該方法可實現更好的性能,由於讀路徑的開銷僅僅涉及 volatile 讀操做,這一般要優於一個無競爭的鎖獲取的開銷。


清單 6. 結合使用 volatile 和 synchronized 實現 「開銷較低的讀-寫鎖」 

@ThreadSafepublic class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; }}

之因此將這種技術稱之爲 「開銷較低的讀-寫鎖」 是由於您使用了不一樣的同步機制進行讀寫操做。由於本例中的寫操做違反了使用 volatile 的第一個條件,所以不能使用 volatile 安全地實現計數器 —— 您必須使用鎖。然而,您能夠在讀操做中使用 volatile 確保當前值的可見性,所以可使用鎖進行全部變化的操做,使用 volatile 進行只讀操做。其中,鎖一次只容許一個線程訪問值,volatile 容許多個線程執行讀操做,所以當使用 volatile 保證讀代碼路徑時,要比使用鎖執行所有代碼路徑得到更高的共享度 —— 就像讀-寫操做同樣。然而,要隨時牢記這種模式的弱點:若是超越了該模式的最基本應用,結合這兩個競爭的同步機制將變得很是困難。

結束語

與鎖相比,Volatile 變量是一種很是簡單但同時又很是脆弱的同步機制,它在某些狀況下將提供優於鎖的性能和伸縮性。若是嚴格遵循 volatile 的使用條件 —— 即變量真正獨立於其餘變量和本身之前的值 —— 在某些狀況下可使用 volatile 代替 synchronized 來簡化代碼。然而,使用 volatile 的代碼每每比使用鎖的代碼更加容易出錯。本文介紹的模式涵蓋了可使用 volatile 代替synchronized 的最多見的一些用例。遵循這些模式(注意使用時不要超過各自的限制)能夠幫助您安全地實現大多數用例,使用 volatile 變量得到更佳性能。

引自:http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

 

Synchronized(obj) 關鍵字指示此obj對象正在被使用,請等待.

狀況1:方法內使用,確保調用方法的若干線程能夠獨立使用obj。

狀況2:修飾類的成員函數,語義等價位synchronized(this) ,即該對象被標記爲synchronized的方法須要互斥執行。

狀況3:修飾類的靜態函數,至關於修飾class對象,即類的靜態變量須要被互斥訪問。

如上所述,鎖須要提供原子性和可見性,volatile變量不被存放在寄存器內,沒有別的副本,虛擬機在使用volatitle變量時老是特別當心的去內存中從新讀取惟一資源,因此它對於任何讀操做都是可見的,而它不能保證在其上的操做時原子性的,由於線程時刻會被掛起,而別的線程將會擁有volatile的寫權限。

如此可見,不管如何在有狀態即共享內存的應用中,volatile仍是有用處的。由於它提供了可見性。互斥與可見性是不一樣的需求。當在內存中共享數據時,可見性需求天然就浮現出來了。但它對互斥沒有任何幫助。它只能做用於數據,不能做用於方法(或方法中的代碼)。上文提到將volatile與synchronized結合能夠做爲較低開銷的讀寫鎖來用。這個好象不太合適。由於讀寫鎖屬於第三級封鎖,它解決了事務的前三個問題:更新失敗,髒讀,不可重複讀。可是volatile並非讀鎖,因此它並不由止寫時的併發讀操做,這致使兩個問題:1,髒讀;2,不可重複讀。也就是說,從事務的角度看,volatile與synchronized的結合,並不比單獨用synchronized好到哪裏去。事實上,它不能解決任何單獨使用synchronized時已經解決之外的問題。可是,它明明幫助了,不是嗎?

它是幫助了,可是不是在事務的角度幫助的。事務要求隔離。要求一致的數據基礎。即便在數據庫之外去討論這個問題,對這些要求的知足仍然是必要的。由於事務做爲一個程序單元,它表明了程序的ACID語義。數據庫管理系統是程序,由於它是一個程序。只要有數據的地方,就有ACID語義的存在。事實上,事務內存即爲此而生。只不過,根據網上的文章,由於找不到太多殺手級應用,一些大型軟件廠商好比微軟已經中止了對它的研究。

可是微軟歷來就不是爲正確性工做的,他們是爲錢工做的。

全部的事務問題(更新失敗,髒讀,不可重複讀,幻影讀),事務隔離級別(封鎖協議),都依賴於軟件價值系統中的ACID語義要求。

volatile在事務之外幫助了synchronized。或者說,至少,它幫助語言得到了正確性(數據)。

What a Big Deal!?...Why I have to tell machines if one program will be run in concurrent environment? Cause I think I'll only be in charge of implementing logics. Since when I started to tell the machine how to do their job? Because I've been always thinking that I just give or desribe the job to the computer and then take a rest. Isn't it supposed to be? Doing the things in the right way isn't the job of OS at the first place? What is an OS? It's Operating what? and to achieve what?

I'm really, really confused...

相關文章
相關標籤/搜索