JavaRush /Java Blog /Random-TW /管理波動性
lexmirnov
等級 29
Москва

管理波動性

在 Random-TW 群組發布

使用易失性變數的指南

作者:布萊恩‧戈茨 2007 年 6 月 19 日 原文:管理波動性 Java中的揮發性變數可以稱為「synchronized-light」;它們比同步區塊需要使用更少的程式碼,通常運行速度更快,但只能完成同步區塊功能的一小部分。本文介紹了有效使用 volatility 的幾種模式,以及一些關於哪些地方不應該使用它的警告。鎖有兩個主要特徵:互斥(mutex)和可見性。互斥意味著鎖一次只能由一個執行緒持有,這一特性可用於實現共享資源的存取控制協議,以便一次只有一個執行緒使用它們。可見性是一個更微妙的問題,其目的是確保在釋放鎖之前對公共資源所做的更改對於接管該鎖的下一個執行緒是可見的。如果同步不能保證可見性,則執行緒可能會收到過時或不正確的公共變數值,這將導致許多嚴重問題。
波動變數
易失性變數具有同步變數的可見性屬性,但缺乏同步變數的原子性。這意味著執行緒將自動使用易失性變數的最新值。它們可用於線程安全但在非常有限的情況下:那些不引入多個變數之間或變數的當前值和未來值之間的關係的情況。因此,僅使用 volatile 不足以實現計數器、互斥體或任何其不可變部分與多個變數關聯的類別(例如,「start <=end」)。您可以出於兩個主要原因之一選擇易失性鎖:簡單性或可擴展性。當某些語言結構使用易失性變數而不是鎖定時,它們更容易編寫為程式碼,並且隨後更容易閱讀和理解。此外,與鎖不同,它們不能阻塞線程,因此不太容易出現可擴展性問題。在讀取次數多於寫入次數的情況下,與鎖定相比,易失性變數可以提供效能優勢。
正確使用揮發性物質的條件
在有限的情況下,您可以將鎖替換為易失性鎖。為了線程安全,必須滿足兩個條件:
  1. 寫入變數的內容與其目前值無關。
  2. 此變數不參與其他變數的不變量。
簡單地說,這些條件意味著可以寫入 volatile 變數的有效值獨立於程式的任何其他狀態,包括變數的當前狀態。第一個條件排除使用易失性變數作為執行緒安全計數器。雖然增量(x++)看起來像一個單一的操作,但它實際上是必須以原子方式執行的整個讀取-修改-寫入操作序列,而揮發性不提供這一點。有效的操作要求 x 的值在整個操作過程中保持不變,而使用 volatile 無法實現這一點。(但是,如果可以確保僅從一個執行緒寫入該值,則可以省略第一個條件。)在大多數情況下,第一個或第二個條件都會被違反,這使得 volatile 變數成為比同步變數更不常用的實作執行緒安全的方法。清單 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; } } 由於範圍狀態變數以這種方式受到限制,因此僅使下域和上域欄位為 volatile 不足以確保類別是線程安全的;仍然需要同步。否則,遲早你會不幸,兩個執行緒使用不適當的值來執行 setLower() 和 setUpper() 可能會導致範圍達到不一致的狀態。例如,如果初始值為(0, 5),線程A調用setLower(4),同時線程B調用setUpper(3),這些交錯的操作將導致錯誤,儘管兩者都會通過檢查這應該是為了保護不變量。因此,範圍將為 (4, 3) - 不正確的值。我們需要使 setLower() 和 setUpper() 對於其他範圍操作來說是原子的 - 而使字段成為 volatile 則無法做到這一點。
性能考慮因素
使用 volatile 的第一個原因是簡單。在某些情況下,使用這樣的變數比使用與其關聯的鎖更容易。第二個原因是效能,有時易失性會比鎖更快。做出像「X 總是比 Y 快」這樣精確、包羅萬象的陳述是極其困難的,特別是當涉及到 Java 虛擬機器的內部操作時。(例如,在某些情況下,JVM 可能會完全釋放鎖,這使得以抽象方式討論 揮發性與同步的成本變得困難)。然而,在大多數現代處理器架構上,讀取易失性變數的成本與讀取常規變數的成本沒有太大區別。由於可見性所需的記憶體防護,寫入易失性的成本明顯高於寫入常規變量,但通常比設定鎖便宜。
正確使用易失性的模式
許多並發專家傾向於完全避免使用揮發性變量,因為它們比鎖更難正確使用。然而,有一些明確定義的模式,如果仔細遵循,可以在各種情況下安全地使用。始終尊重易失性的限制 - 只使用獨立於程式中其他任何內容的易失性,這應該可以防止您進入這些模式的危險領域。
模式#1:狀態標誌
也許可變變數的規範使用是簡單的布林狀態標誌,指示已發生重要的一次性生命週期事件,例如初始化完成或關閉請求。許多應用程式都包含以下形式的控制結構:“直到我們準備好關閉,繼續運行”,如清單 2 所示: volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } shutdown() 方法很可能會從循環之外的某個地方(在另一個執行緒上)呼叫。因此需要同步以確保正確的變數可見性 shutdownRequested。(它可以從 JMX 偵聽器、GUI 事件執行緒中的操作偵聽器、透過 RMI、透過 Web 服務等呼叫)。然而,具有同步區塊的循環將比具有易失性狀態標誌的循環麻煩得多,如清單2 所示。因為易失性使編寫程式碼更容易,並且狀態標誌不依賴任何其他程式狀態,所以這是一個善用揮發性。此類狀態標誌的特徵是通常只有一次狀態轉換;shutdownRequested 標誌從 false 變成 true,然後程式關閉。這種模式可以擴展到可以來回改變的狀態標誌,但前提是在沒有外部幹預的情況下發生轉換週期(從假到真到假)。否則需要某種原子轉換機制,例如原子變數。
模式#2:一次性安全發布
當編寫物件參考而不是原始值時,沒有同步時可能出現的可見性錯誤可能會成為更加困難的問題。如果沒有同步,您可以看到另一個執行緒寫入的物件參考的目前值,並且仍然可以看到該物件的陳舊狀態值。(這種威脅是臭名昭著的雙重檢查鎖定問題的根源,在這種情況下,在沒有同步的情況下讀取物件引用,並且您可能會看到實際引用,但透過它獲取部分構造的物件。 )一種安全發布物件的方法object 是對易失性物件的參考。清單 3 顯示了一個範例,其中在啟動期間,後台執行緒從資料庫載入一些資料。其他可能嘗試使用此資料的程式碼會在嘗試使用它之前檢查它是否已發布。 public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // делаем много всякого theFlooble = new Flooble(); // единственная запись в theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // чё-то там делаем... // используем theFolooble, но только если она готова if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } } 如果對 theFlooble 的引用不是易失性的,則 doWork() 中的程式碼在嘗試引用 theFlooble 時將面臨看到部分建構的 Flooble 的風險。此模式的關鍵要求是發布的物件必須是線程安全的或有效不可變的(有效不可變意味著其狀態在發布後永遠不會改變)。易失性連結可確保物件以其發布形式可見,但如果物件的狀態在發布後發生變化,則需要額外的同步。
模式#3:獨立觀察
安全使用 volatility 的另一個簡單例子是定期「發布」觀察結果以在程式中使用。例如,有一個環境感測器可以檢測當前的溫度。後台執行緒可以每隔幾秒鐘讀取該感測器並更新包含當前溫度的易失性變數。然後其他線程可以讀取該變量,因為知道其中的值始終是最新的。此模式的另一個用途是收集有關程序的統計資料。清單 4 顯示了身份驗證機制如何記住最後登入使用者的名稱。LastUser 引用將被重複使用來發佈該值以供程式的其餘部分使用。 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:「揮發性豆」模式
「易失性 bean」模式適用於使用 JavaBean 作為「美化結構」的框架。「易失性 bean」模式使用 JavaBean 作為一組具有 getter 和/或 setter 的獨立屬性的容器。需要「易失性 bean」模式的基本原理是,許多框架為可變資料持有者(例如 HttpSession)提供容器,但放置在這些容器中的物件必須是執行緒安全的。在易失性 bean 模式中,所有 JavaBean 資料元素都是易失性的,而且 getter 和 setter 應該很簡單 - 除了取得或設定相應的屬性之外,它們不應包含任何邏輯。此外,對於作為物件所引用的資料成員,所述物件必須是有效不可變的。(這不允許數組引用字段,因為當數組引用被聲明為易失性時,只有該引用而不是元素本身俱有易失性屬性。)與任何易失性變量一樣,不能有與JavaBean 屬性相關的不變數或限制。清單 5 顯示了使用「易失性 bean」模式編寫的 JavaBean 範例: @ThreadSafe public 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 可以提供效能或可擴展性優勢。更高級的波動模式可能極為脆弱。仔細記錄您的假設並且嚴格封裝這些模式至關重要,因為即使是最小的更改也可能會破壞您的程式碼!此外,考慮到更複雜的易失性用例的主要原因是效能,請確保在使用它們之前您確實對預期的效能增益有明確的需求。這些模式是為了可能的性能提升而犧牲可讀性或易於維護性的妥協 - 如果您不需要性能改進(或者無法通過嚴格的測量程序證明您需要它),那麼這可能是一個糟糕的交易,因為你放棄了一些有價值的東西,卻得到了一些較少的回報。
模式#5:廉價的讀寫鎖
現在您應該清楚地意識到,揮發性太弱,無法實現計數器。由於 ++x 本質上是三個操作(讀取、追加、儲存)的減少,因此如果出現問題,如果多個執行緒嘗試同時遞增易失性計數器,您將遺失更新的值。但是,如果讀取次數明顯多於更改次數,您可以結合內部鎖定和揮發性變數來減少總體程式碼路徑開銷。清單 6 顯示了一個線程安全的計數器,它使用 synchronized 來確保增量操作是原子的,並使用 volatile 來確保當前結果是可見的。如果更新不頻繁,這種方法可以提高效能,因為讀取成本僅限於易失性讀取,這通常比取得非衝突鎖定便宜。 @ThreadSafe public 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。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION