java並發編程實戰之執行緒安全性(一)
1.1什麼是執行緒安全性
要對執行緒安全性給出一個確切的定義是非常複雜的。最核心的概念就是正確性。正確性:某個類的行為與其規範完全一致。在良好的規範中通常會定義各種不變性條件來約束對象的狀態,以及定義各種後驗條件來描述對象操作的結果。
由於我們通常定義一個類的時候不會編寫詳細的規範,因此我們可以把單執行緒的正確性近似定義為:可見即所知。對於正確性有了較為清晰的定義後,就可以定義執行緒安全性:當多個執行緒訪問某個類時(不管運行時環境採用何種調度方式或者這些執行緒如何交替執行,並且
在主調程式碼中不需要任何額外的同步或協同),這個類始終都能表現出正確的行為,那麼就稱這個類是執行緒安全的。
實例:一個無狀態的servlet
無狀態的:它既不包含任何域,也不包含任何對其他類中域的引用。計算過程中的臨時狀態僅存於執行緒棧上的局部變數,並且只能由正在執行的執行緒訪問。 無狀態對象一定是執行緒安全的,大多數servlet都是無狀態的
1.2 原子性
在並發編程中:由於不恰當的執行時序而出現不正確的結果是一種非常重要的結果,它有一個正式的名字:競態條件(Race condition)
1.2.1 競態條件:
最常見的競態條件類型就是先檢查後執行,通過一個可能失效的觀測結果來決定下一步動作。使用先檢查後執行的一種常見情況就是延遲初始化,延遲初始化的目的就是將對象初始化操作推遲到實際被使用時才進行,同時保證只被初始化一次。
實際情況中,應儘可能地使用現有的執行緒安全對象(例如acomiclong)來管瘤類的狀態。與非執行緒相比,判斷執行緒安全對象的可能狀態及其狀態轉換情況要更為容易,從而也更容易維護和驗證執行緒安全性。
1.3 加鎖機制
要保持狀態的一致性,就需要在單子原子操作中更新所有相關的狀態變數
1.3.1 內置鎖
Java提供了一種內置的鎖機制來支援原子性:同步程式碼塊(Synchronized Block) 同步程式碼塊包含了倆部分:一個是作為鎖的對象引用,一盒作為由這個鎖保護的程式碼塊。以關鍵字synchronized來修飾的方法就是一種橫跨整個方法體的同步程式碼塊,其中該同步程式碼塊的鎖就是方法調用所在的對象,靜態的synchronized 方法以class對象作為鎖
synchronized(lock){
//訪問或者修改由鎖保護的共享狀態
}
每個Java對象都可以用做一個實現同步的鎖,這些鎖被稱為內置鎖或者監視器鎖,執行緒在進入同步程式碼塊的時候會自動獲取鎖,並且在退出同步程式碼塊的時候會自動釋放鎖,而無論是通過正常的控制路徑退出,還是通過從程式碼塊中異常退出,獲取內置鎖的唯一途徑就是進入由鎖保護的同步程式碼塊或方法。
Java的內置鎖相當於一種互斥體(互斥鎖),這意味著最多只要有一個執行緒能持有這種鎖,有些場景並發性不是很好。
1.3.2 重入
當某個執行緒請求一個一個由其他執行緒持有的鎖時,發出請求的執行緒就會阻塞。然而,由於內置鎖是可以重入的,因此如果某個執行緒試圖獲得一個已經由它自己持有的鎖,那麼這個請求就會成功,重入意味著獲取鎖的操作的粒度是執行緒,而不是調用。重入的一種實現方式是,為每個鎖關聯一個獲取技術值和所有者執行緒,計數值為0的時候表示該鎖沒有被持有 計數值可以累加 退出時會累減。
2.4 用鎖來保護狀態
一種常見的錯誤是認為,只有在寫入共享變數時才需要使用同步,然而事實並非如此。對於可能被多個執行緒同時訪問的可變狀態變數,在訪問它時都需要持有同一個鎖,在這種情況下,我們稱狀態變數是由這個鎖保護的。
一種常見的加鎖約定是,將所有的可變狀態都封裝在對象內部,並通過對象的內置鎖對所有的訪問可變狀態的程式碼路徑進行同步,使得在該對象上不會發生並發訪問。在許多執行緒安全類都使用了這種模式,
2.5活躍性與性能
通常,在簡單性與性能之間存在相互制約的因素,不能盲目為了性能而犧牲簡單性,因為可能破壞安全性,當執行時間較長的計算或者可能無法快速完成的操作時,一定不要持有鎖,例如網路IO或者控制台IO