1.線程安全性
線程安全類
當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼稱這個類是線程安全的。
正確性
某個類的行爲與其規範完全一致。在良好的規範中通常會定義各種不變性條件(Invariant)來約束對象的狀態,以及定義各種後驗條件(Postcondition)來描述對象的操作結果。
比如說在Vector中,get(i)
時應該先判斷i >= 0 && i < size
,否則應該拋出ArrayIndexOutOfBoundsException
異常。
不是所有的時候都會爲代碼編寫詳細的規範,所以退而求其次,以代碼可信性
來間接表述代碼的正確性。 在單線程程序中,正確性近似等於所見即所知
,即在正確的單線程程序中,通過輸入是可以確定輸出結果。那麼,線程安全類也可以理解爲在併發環境和單線程環境中都不會被被破壞的類,在一定程度上可以確定輸出結果。
有狀態對象
擁有數據存儲功能的對象,擁有自己的數據域或者對其他對象的引用,通常數據不同的對象會表現出不同的狀態。
比如在開發過程中使用的實體對象
、JavaBean
等對象,可以稱爲有狀態對象,還有一些既包含數據又包含複雜操作的對象,比如List
、Map
等類型的對象。
無狀態對象
不存儲數據的對象,通常是一系列操作的集合。這種對象既不包含任何域,也不包含任何對其他類中域的引用。
比如在開發過程中使用的Controller
、Service
等類型的對象,這些對象只有一些操作,通常在系統中還會以單例的方式存在。
無狀態對象一定是線程安全的。
在無狀態類中添加一個狀態時,如果該狀態完全是由線程安全的對象來管理,那麼這個類仍然是線程安全的。但是,當狀態數量由一個變爲多個時,尤爲需要注意,此時並不能像狀態數量由0個變爲1個時那樣簡單的成爲線程安全類。
在開發過程中,應儘可能地使用現有的線程安全對象(例如
AtomicLong
)來管理類的狀態。
2.原子性
原子(atomic)操作
如果這個操作所處的層(layer)的更高層不能發現其內部實現與結構,那麼這個操作就是原子操作。原子操作可以是一個步驟,也可以是多個操作步驟,但是順序不可以被打亂,也不可以被切割而只執行其中的一部分。原子操作一旦開始執行,就一直運行到結束。將整個操作視作一個操作是原子性的的核心特徵。
在Java中,把不會被線程調度所打斷的操作稱之爲原子操作,比如說在對非long
型和非double
型變量的讀寫操作就是原子操作。
競態條件(Race Condition)
由於不恰當的執行時序而出現不正確的結果。
當某個操作的正確性取決於多個線程的交替執行時序時,那麼這種情況就稱爲爲競態條件。等同於獲得正確的結果完全看運氣。兩種常見的競態條件是先檢查後執行(Check-Then-Act)
和讀取-修改-寫入
。
數據競爭
如果在訪問共享的非final類型的域時沒有采用同步來進行協同,那麼就會出現數據競爭。
比如當一個線程寫入一個變量而另一個線程接下來讀取這個變量,或者讀取一個之前由另一個線程寫入的變量時,並且在這個兩個線程之間沒有使用同步,那麼可能會出現數據競爭。
數據競爭會產生類似於事務操作過程中的髒讀
、不可重複讀
、幻讀
等問題。
3.加鎖機制
在線程安全性的定義要求中,多個線程之間的操作無論採用何種執行時序或交替方式,都要保證不變性條件不被破壞。爲了保證不變性條件不被破壞,就需要使用鎖來進行保護。
要保持狀態的一致性,就需要在單個原子操作中更新所有相關的狀態變量。
同步代碼塊(Synchronized Blcok)
同步代碼塊包括兩部分:一個作爲鎖的對象引用,一個作爲由這個鎖保護的代碼塊。每個Java對象
都可以用做一個實現同步的鎖,這些鎖被稱爲內置鎖(Intrinsic Lock)
,內置鎖是互斥鎖。
線程在進入同步代碼塊之前會自動獲得鎖,並且在退出同步代碼塊時自動釋放鎖(包括正常退出和拋出異常後退出)。獲得內置鎖的唯一途徑就是進入由這個鎖保護的同步代碼塊或方法。
重入
如果某個線程試圖獲取一個已經由它自己持有的鎖,那麼這個請求會成功。
重入的一種實現方法是,爲每個鎖關聯一個獲取計數值和一個所有者線程。當計數值爲0時,這個鎖被認爲是沒有被任何線程持有。當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,並且將獲取計數值置爲1.如果同一個線程再次獲取這個鎖,計數值將遞增,而當線程退出同步代碼塊時,計數器會相應地遞減。當計數值爲0時,這個鎖將被釋放。
內置鎖是可重入的。
用鎖來保護狀態
對於可能被多個線程同時訪問的可變狀態變量,在訪問它時都需要持有同一個鎖,在這種情況下,稱狀態變量是由這個鎖保護的。每個共享的和可變的變量都應該只由一個鎖保護,便於維護。
對於每個包含多個變量的不變性條件,其中涉及所有變量都需要由同一個鎖來保護。
當執行時間較長的計算或者可能無法快速完成的操作時(例如網絡I/O),一定不要持有鎖。