Volatile變量
在程序設計中,尤其是在C語言、C++、C#和Java語言中,使用volatile關鍵字聲明的變量或對象通常擁有和優化和(或)多線程相關的特殊屬性。通常,volatile關鍵字用來阻止(偽)編譯器對某些其認為無法“被代碼本身”改變的代碼(變量/對象)進行優化。如在C語言中,volatile關鍵字可以用來提醒編譯器它后面所定義的變量隨時有可能改變,因此編譯后的程序每次需要存儲或讀取這個變量的時候,都會直接從變量地址中讀取數據。如果沒有volatile關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,如果這個變量由別的程序更新了的話,將出現不一致的現象。 在C環境中,volatile關鍵字的真實定義和適用范圍經常被誤解。雖然C++、C#和Java都從C中神秘地“繼承”了volatile,在這些編程語言中volatile的用法和語義卻大相徑庭。【維基百科】
通俗點講解上面這句話,意思就是,編譯器為了快速讀寫,會將數據放到寄存器緩存中,而使用了volatile修飾以后,告訴編譯器,不要對該變量進行優化,每次讀取都從內存中去讀取。
鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。互斥即一次只允許一個線程持有某個特定的鎖,因此可使用該特性實現對共享數據的協調訪問協議,這樣,一次就只有一個線程能夠使用該共享數據。可見性要更加復雜一些,它必須確保釋放鎖之前對共享數據做出的更改對于隨后獲得該鎖的另一個線程是可見的 —— 如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發許多嚴重問題。
Volatile 變量具有 synchronized 的可見性特性,但是不具備原子特性。這就是說線程能夠自動發現 volatile 變量的最新值。Volatile 變量可用于提供線程安全,但是只能應用于非常有限的一組用例:多個變量之間或者某個變量的當前值與修改后值之間沒有約束。因此,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具有與多個變量相關的不變式(Invariants)的類
1.1 正確使用 volatile 變量的條件
您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:
- 對變量的寫操作不依賴于當前值。
- 該變量沒有包含在具有其他變量的不變式中。
第一個條件的限制使 volatile 變量不能用作線程安全計數器。雖然增量操作(x++
)看上去類似一個單獨操作,實際上它是一個由讀取-修改-寫入操作序列組成的組合操作,必須以原子方式執行,而 volatile 不能提供必須的原子特性。實現正確的操作需要使 x
的值在操作期間保持不變,而 volatile 變量無法實現這點。(然而,如果將值調整為只從單個線程寫入,那么可以忽略第一個條件。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
清單 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 類型是無法實現這一目的的。
1.2 性能考慮
很難做出準確、全面的評價,例如 “X 總是比 Y 快”,尤其是對 JVM 內在的操作而言。(例如,某些情況下 VM 也許能夠完全刪除鎖機制,這使得我們難以抽象地比較 volatile
和 synchronized
的開銷。)就是說,在目前大多數的處理器架構上,volatile 讀操作開銷非常低 —— 幾乎和非 volatile 讀操作一樣。而 volatile 寫操作的開銷要比非 volatile 寫操作多很多,因為要保證可見性需要實現內存界定(Memory Fence),即便如此,volatile 的總開銷仍然要比鎖獲取低。
很多并發性專家事實上往往引導用戶遠離 volatile 變量,因為使用它們要比使用鎖更加容易出錯。然而,如果謹慎地遵循一些良好定義的模式,就能夠在很多場合內安全地使用 volatile 變量。要始終牢記使用 volatile 的限制 —— 只有在狀態真正獨立于程序內其他內容時才能使用 volatile —— 這條規則能夠避免將這些模式擴展到不安全的用例。
1.3 正確使用 volatile 的模式
1
2
3
4
5
6
7
8
9
10
11
12
|
清單 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)。此外,還需要某些原子狀態轉換機制,例如原子變量。
與鎖相比,Volatile 變量是一種非常簡單但同時又非常脆弱的同步機制,它在某些情況下將提供優于鎖的性能和伸縮性。如果嚴格遵循 volatile 的使用條件 —— 即變量真正獨立于其他變量和自己以前的值 —— 在某些情況下可以使用 volatile 代替 synchronized 來簡化代碼。然而,使用 volatile 的代碼往往比使用鎖的代碼更加容易出錯。本文介紹的模式涵蓋了可以使用 volatile 代替 synchronized 的最常見的一些用例。遵循這些模式(注意使用時不要超過各自的限制)可以幫助您安全地實現大多數用例,使用 volatile 變量獲得更佳性能。
原文地址:https://www.cnblogs.com/CBDoctor/p/5143111.html