併發程式設計實戰(2):原子性、可見性和競態條件與複合操作
原子性
一個不可分割的操作,比如a=0;再比如:a++; 這個操作實際是a = a + 1;是可分割的,它其實包含三個獨立的操作:讀取a的值,將值加1,然後將計算結果寫入a,這是一個“讀取-修改-寫入”的操作序列,所以他不是一個原子操作。
可見性
可見性,是指執行緒之間的可見性,一個執行緒修改的狀態對另一個執行緒是可見的。也就是一個執行緒修改的結果,另一個執行緒馬上就能看到。
比如:用volatile修飾的變數,就會具有可見性。volatile修飾的變數不允許執行緒內部快取和重排序,即直接修改記憶體。所以對其他執行緒是可見的。但是這裡需要注意一個問題,volatile只能讓被他修飾內容具有可見性,但不能保證它具有原子性。比如 volatile int a = 0;之後有一個操作 a++;這個變數a具有可見性,但是a++ 依然是一個非原子操作,也就這這個操作同樣存線上程安全問題。
關係
原子性是說一個操作是否可分割。可見性是說操作結果其他執行緒是否可見。
競態條件
在併發程式設計中,由於不恰當的執行時序而出現不正確的結果是一種非常重要的情況,被稱為競態條件(race condition)
最常見的競態條件:先檢查後執行(Check-Then-Act),即通過一個可能失效的觀測結果來決定下一步的動作:首先觀察到某個條件為真(例如檔案X不存在),然後根據這個觀察結果採用相應的動作(建立檔案X),但事實上在觀察到這個結果以及開始建立檔案之前,觀察結果可能變得無效(另一個執行緒在這期間建立了檔案X),從而導致各種問題(未預期的異常、資料被覆蓋、檔案被破壞等)。
最常見的競態條件:延遲初始化,比如檢查到某個例項為null,然後初始化例項
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null) {
instance = new ExpensiveObject();
}
return instance;
}
}
另一種競態條件: “讀取-修改-寫入”操作(例如遞增一個計數器)
基於物件之前的狀態來定義物件狀態的轉換
複合操作
要避免競態條件問題,就必須在某個執行緒修改該變數時,通過某種方式防止其他執行緒使用這個變數,從而確保其他執行緒只能在修改操作完成之前火之後讀取和修改狀態,而不是在修改狀態的過程中。
一般將“先檢查後執行”、“讀取-修改-寫入”等操作統稱為複合操作:包含了一組以原子方式執行的操作以確保執行緒安全性。