ITEM 82: DOCUMENT THREAD SAFETY
??當(dāng)類的方法被并發(fā)使用時,類的行為是它與客戶端的契約的一個重要部分。如果您沒有記錄類的這方面行為,它的用戶將被迫做出假設(shè)。如果這些假設(shè)是錯誤的,結(jié)果程序可能執(zhí)行不充分的同步(item 78) 或過度同步(item 79)。無論哪種情況,都可能導(dǎo)致嚴(yán)重的錯誤。
??您可能聽說過,可以通過在一個方法的文檔中查找 synchronized 修飾符來判斷該方法是否是線程安全的。這在幾個方面是錯誤的。在正常的操作中,Javadoc 在其輸出中不包含同步修飾符,這是有充分理由的。同步修飾符在方法聲明中的出現(xiàn)是一個實現(xiàn)細(xì)節(jié),而不是其 API 的一部分。它不能可靠地表示方法是線程安全的。
??此外,synchronized 修飾符的存在足以說明線程安全性,這種說法體現(xiàn)了一種誤解,即線程安全性是一個全有或全無的屬性。實際上,線程安全有幾個級別。要啟用安全的并發(fā)使用,類必須清楚地記錄它支持的線程安全級別。下面的列表總結(jié)了線程安全級別。本報告并非詳盡無遺,但涵蓋常見個案:
??? 不變的 —— 這個類的實例看起來是不變的。不需要外部同步。示例包括String、Long和BigInteger (item 17)。
??? 無條件線程安全的類的實例是可變的,但是類有足夠的內(nèi)部同步,它的實例可以并發(fā)使用而不需要任何外部同步。示例包括 AtomicLong 和 ConcurrentHashMap。
??? 有條件線程安全 —— 類似無條件線程安全,除了一些方法需要外部同步來安全并發(fā)使用。示例包括 Collections.synchronized ,其迭代器需要外部同步。
??? 非線程安全的 —— 實例是可變的。要并發(fā)地使用它們,客戶端必須將每個方法調(diào)用(或調(diào)用序列)與客戶端選擇的外部同步放在一起。示例包括通用的集合實現(xiàn),比如ArrayList 和 HashMap。
??? 線程不安全的 —— 這個類對于并發(fā)使用是不安全的,即使每個方法調(diào)用都被外部同步包圍。線程敵意通常是由于修改靜態(tài)數(shù)據(jù)而沒有同步造成的。沒有人是故意編寫線程敵對類的;此類類通常是由于沒有考慮并發(fā)性而產(chǎn)生的。當(dāng)一個類或方法被發(fā)現(xiàn)是線程不相容的,它通常是固定的或不贊成使用的。
??如果沒有內(nèi)部同步,item 78 中的 generateSerialNumber 方法將是線程不安全的,如322頁所討論的。
??這些類別(除了線程不安全的之外) 大致對應(yīng)于 Java 并發(fā)性中的線程安全注釋,它們是 不可變的、ThreadSafe 和 NotThreadSafe [Goetz06,附錄A]。上述分類法中的無條件線程安全和有條件線程安全類別都在 ThreadSafe 注釋中涉及。
??記錄一個有條件線程安全的類需要小心。您必須指出哪些調(diào)用序列需要外部同步,以及必須獲取哪些鎖(在極少數(shù)情況下是鎖)才能執(zhí)行這些序列。通常是實例本身上的鎖,但也有例外。例如,Collections.synchronizedMap 的文檔說:
??當(dāng)用戶在其集合視圖上迭代時,必須手動同步返回的映射:
Map<K, V> m = Collections.synchronizedMap(new HashMap<>());
Set<K> s = m.keySet(); // Needn't be in synchronized block
...
synchronized(m) {
// Synchronizing on m, not s!
for (K key : s)
key.f();
}
??如果不遵循此建議,可能會導(dǎo)致不確定性行為。
??類的線程安全描述通常屬于類的文檔注釋,但是具有特殊線程安全屬性的方法應(yīng)該在它們自己的文檔注釋中描述這些屬性。沒有必要記錄枚舉類型的不變性。除非從返回類型可以明顯看出,靜態(tài)工廠必須記錄返回對象的線程安全性,如 Collections.synchronizedMap 演示的那樣。
??當(dāng)類提交使用公共可訪問的鎖時,它允許客戶端原子地執(zhí)行一系列方法調(diào)用,但是這種靈活性是有代價的。它與諸如 ConcurrentHashMap 等并發(fā)集合所使用的高性能內(nèi)部并發(fā)控制不兼容。另外,客戶機(jī)可以通過長時間持有公共可訪問鎖來發(fā)起拒絕服務(wù)攻擊。這可以是無意的,也可以是有意的。
??為了防止這種拒絕服務(wù)攻擊,你可以使用一個私有鎖對象,而不是使用同步方法(這意味著一個公共可訪問的鎖):
// Private lock object idiom - thwarts denial-of-service attack
private final Object lock = new Object();
public void foo() {
synchronized(lock) {
...
}
}
??因為私有鎖對象在類之外不可訪問,所以客戶端不可能干擾對象的同步。實際上,我們通過將鎖對象封裝在它同步的對象中來應(yīng)用 item 15 的通知。
??注意,鎖字段被聲明為 final。這樣可以防止您無意中更改其內(nèi)容,從而導(dǎo)致災(zāi)難性的非同步訪問(item 78)。通過最小化鎖字段的可變性,我們應(yīng)用了 item 17 的建議。鎖字段應(yīng)該總是生命為 final。無論您使用普通的監(jiān)視器鎖(如上所示)還是java.util.concurrent.locks 提供的鎖工具。
??私有鎖對象習(xí)慣用法只能在無條件線程安全的類上使用。有條件線程安全類不能使用這種習(xí)慣用法,因為它們必須記錄在執(zhí)行某些方法調(diào)用序列時,它們的客戶端要獲取哪些鎖。
??私有鎖對象習(xí)慣用法特別適合為繼承而設(shè)計的類(item 19)。如果這樣的類要使用它的實例進(jìn)行鎖定,那么子類就可以很容易地、無意地干擾基類的操作,反之亦然。由于將同一個鎖用于不同的目的,子類和基類最終可能會“踩到對方的腳”?!斑@不僅僅是一個理論問題;它發(fā)生在 Thread 類[Bloch05, Puzzle 77] 上。
??總而言之,每個類都應(yīng)該用嚴(yán)謹(jǐn)?shù)奈淖置枋龌蚓€程安全注釋清楚地記錄其線程安全屬性。同步修飾符在本文檔中不扮演任何角色。有條件線程安全的類必須記錄哪些方法調(diào)用序列需要外部同步,以及在執(zhí)行這些序列時需要獲取哪些鎖。如果您編寫了無條件線程安全的類,請考慮使用私有鎖對象來代替同步方法。這可以保護(hù)您不受客戶機(jī)和子類的同步干擾,并為您在以后的版本中采用復(fù)雜的并發(fā)控制方法提供了更大的靈活性。