理解並正確使用synchronized和volatile
執行緒安全需要同時滿足三個條件:
- 原子性
某個操作是不可中斷的,且要麼全部做完要麼沒有執行。 - 可見性
通過volatile關鍵字修飾變數實現。讀取volatile變數時,先失效本地快取再讀取主存中的最新值;更新volatile變數會立即將最新值刷回主存。 - 有序性
JMM的happens-before原則。
volatile能保證其修飾的變數的執行緒可見性但無法保證操作原子性,只能用於”多個變數之間或者某個變數的當前值與修改後值之間沒有約束”的場景。在實現計數器(++count)和由多個變數組成的不變表示式方面,volatile無法勝任。
volatile的底層實現機制是什麼?被volatile修飾的變數在進行寫操作時,處理器會插入一條lock字首的彙編程式碼,做了層”記憶體屏障”,其作用為:
-
- 重排序時不能把後面的指令重排序到記憶體屏障之前的位置
-
- 將當前處理器快取行的資料會寫回到系統記憶體。
-
- 這個寫回記憶體的操作會引起在其他CPU裡快取了該記憶體地址的資料無效。
通過處理器之間的快取一致性協議,當(處理器本)地快取過期後會失效本地快取,當更新該快取時處理器重新從主存load資料到本地快取並執行計算邏輯。
什麼場景下別用volatile?
非執行緒安全的計數器類
自增操作並不是原子的,比如”++count”操作就是三個原子操作的集合:
- 讀取count舊值
- 執行緒上下文彙總設定count新值: count+1
- 將count新值刷回主存並失效其他執行緒上下文的count值
假設thread1和thread2均執行”++count”計數操作,thread1和thread2均執行完2但未執行3,此時thread1和thread2先後將count新值刷回主存,這就產生了執行緒不安全的現象。
只有在狀態真正獨立於程式內其他內容時才能使用volatile。
// 引用
volatile操作不會像鎖一樣造成阻塞,因此,在能夠安全使用volatile的情況下,volatile 可以提供一些優於鎖的可伸縮特性。如果讀操作的次數要遠遠超過寫操作,與鎖相比,volatile 變數通常能夠減少同步的效能開銷。
非執行緒安全的數值範圍類
// 程式碼來源於Brian Goetz的《正確使用 Volatile 變數》一文,本文稍作修改
@NotThreadSafe
public class NumberRange {
private volatile 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;
}
}
假設NumberRange初始化後lower/upper分別為0和4,即數值範圍為[0,4]。此時thread1和thread2分別執行setLower(3)和setUpper(2),最終lower/upper分別被thread1和thread2設定為3和2,數值範圍被更新為[3,2],某種意義上看是一個無效的數值範圍。
什麼場景下可以使用volatile?
1、對變數的寫入操作不依賴變數的當前值,或者你能確保只有單個執行緒更新變數的值。
2、該變數沒有包含在具有其他變數的不變式中。
Case1: 狀態標誌
這種型別的狀態標記的一個公共特性是:通常只有一種狀態轉換。
// 程式碼來源於Brian Goetz的《正確使用 Volatile 變數》一文,本文稍作修改
volatile boolean shutdownRequested;
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
Case2: 一次性安全釋出 – One-time Safe Publication
// 程式碼來源於Brian Goetz的《正確使用 Volatile 變數》一文,本文稍作修改
public class Floor {
public Floor() {
// initialization...
}
}
public class Loader {
public volatile Floor floor;
public void init() {
// this is the only write to floor
floor = new Floor();
// initialize floor object here ...
}
public Floor getFloor() {
return this.floor;
}
}
public class SomeOtherClass {
private Loader loader;
public void doWork() {
while (true) {
// use "floor" only if it is ready
if (loader != null) // 步驟1
doSomething(loader.getFloor()); // 步驟2
}
}
}
這是一個典型的讀寫衝突例子,引文中提到“如果Floor引用不是 volatile 型別,doWork() 中的程式碼在解除對Floor的引用時,將會得到一個不完全構造的Floor”。我對這句理解的是:thread2執行完步驟2後釋放對Floor的引用時,thread1可能正在呼叫init方法初始化floor,此時thread2拿到的是還未被thread1完全初始化的Floor物件。如果doWork內部主動解除對floor物件的引用,則可能拿到未初始化完全的floor物件的引用。
即使floor物件完成初始化,對floor成員域的修改仍然是執行緒不可見的。
volatile引用可以保證任意執行緒都可以看到這個物件引用的最新值,但不保證能看到被引用物件的成員域的最新值。
因為volatile修飾的是floor物件的引用,如果thread1執行到步驟1時,thread3修改了floor成員域,其修改對thread1並不可見。思考:如果floor成員域均被volatile鎖修飾,其成員域的修改是否對thread3可見?
Case3: 多個獨立觀察結果的釋出
// 程式碼來源於Brian Goetz的《正確使用 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;
}
}
這個模式要求被髮布的值(lastUser)是有效不可變的 —— 即值的狀態在釋出後不會更改。與Case1中更新floor物件成員域不同,對String類的操作是在新的String例項上進行的,String物件本身的狀態並未改變。String類的這種實現方式天然地提供了執行緒隔離性。
volatile並非用來解決高併發場景下資料競爭衝突的方案,它只是實現執行緒可見性的一種方式!如果多個執行緒同時更新volatile變數,需要採用同步機制解決資料競爭,如CAS或者鎖等。
Case4: “volatile bean” 模式
該模式中,Java Bean成員變數均被volatile修飾,且引用型別的成員變數也必須是有效不可變(Collection的子類如List, Set, Queue等有陣列值的成員變數,volatile修飾的是陣列引用並非陣列元素!)。
引用文章
相關文章
- 怎樣正確理解volatile?
- synchronized和volatile的區別synchronized
- 正確理解和使用JAVA中的字串常量池Java字串
- 如何正確理解棧和堆?
- 正確理解memcached,才能更好的使用
- 從JMM透析volatile與synchronized原理,圖文並茂synchronized
- 全面解讀volatile和synchronize,輕鬆掌握Volatile與Synchronizedsynchronized
- 如何正確理解「指標」和「標籤」指標
- Volatile關鍵字&&DCL單例模式,volatile 和 synchronized 的區別單例模式synchronized
- 多執行緒基礎之synchronized和volatile執行緒synchronized
- 正確理解CAP理論
- volatile與synchronized的區別synchronized
- 正確理解 PHP 的過載PHP
- background-position的正確理解方式
- Java併發2:JMM,volatile,synchronized,finalJavasynchronized
- volatile的理解
- GIT使用rebase和merge的正確姿勢Git
- 正確的使用pod install 和 pod update - CocoaPods
- 正確高效使用 GoogleGo
- 談如何正確理解 IP 資料的覆蓋率,兼談正確率~
- 打工人,從 JMM 透析 volatile 與 synchronized 原理synchronized
- Go通關04:正確使用 array、slice 和 map!Go
- transient和synchronized的使用synchronized
- 深入理解volatile
- 徹底理解volatile
- 如何正確使用ping呢
- Postman 正確使用姿勢Postman
- PHP Opcache 的正確使用PHPopcache
- 如何正確使用 Slim 框架框架
- 如何正確使用async/await?AI
- volatile和synchronized到底啥區別?多圖文講解告訴你synchronized
- 如何正確理解Python培訓?有必要嗎?Python
- 徹底理解synchronizedsynchronized
- ThreadLocal的正確使用與原理thread
- TiDB 的正確使用姿勢TiDB
- Redis的正確使用姿勢Redis
- Android中Handler的正確使用Android
- java安全編碼指南之:lock和同步的正確使用Java