Effective Java 第三版——82. 線程安全文檔化

Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java

Effective Java, Third Edition

82. 線程安全文檔化

當併發使用一個類的方法時,類的行爲方式是其與客戶端創建約定的重要部分。若是未能文檔化記錄某個類行爲的這一方面,其用戶只能作出作出假設。若是這些假設是錯誤的,則生成的程序可能執行同步不夠(條目 78)或過分同步(條目 79)。 不管哪一種狀況,均可能致使嚴重錯誤。git

你可能據說過,能夠經過在方法的文檔中查找synchronized修飾符來判斷該方法是否線程安全的。這在幾個方面來說是錯誤的。在正常操做中,Javadoc的輸出中沒有包含synchronized修飾符,這是有緣由的。方法聲明中synchronized修飾符的存在是實現細節,而不是其API的一部分。它不能可靠地說明方法是是線程安全的。github

此外,聲稱存在synchronized修飾符就足以文檔記錄線程安全性,這體現了線程安全性是要麼全有要麼全無屬性的誤解。實際上,線程安全有幾個級別。要啓用安全的併發使用,類必須清楚地文檔記錄它支持的線程安全級別。下面的列表總結了線程安全級別。它並不是詳盡無遺,但涵蓋如下常見狀況:安全

  • 不可變的(Immutable) —— 該類的實例看起來是不變的(constant)。不須要外部同步。示例包括String、Long和BigInteger(條目 17)。併發

  • 無條件線程安全(Unconditionally thread-safe) —— 此類的實例是可變的,但該類具備足夠的內部同步,以即可以併發使用其實例而無需任何外部同步。 示例包括AtomicLong和ConcurrentHashMap。性能

  • 有條件線程安全(Conditionally thread-safe) —— 與無條件線程安全同樣,但某些方法須要外部同步以便安全併發使用。 示例包括Collections.synchronized包裝器返回的集合,其迭代器須要外部同步。線程

  • 非線程安全(Not thread-safe) —— 這個類的實例是可變的。 要併發使用它們,客戶端必須使用其選擇的外部同步來包圍每一個方法調用(或調用序列)。 示例包括通用集合實現,例如ArrayList和HashMap。設計

  • 線程對立(Thread-hostile) —— 即便每一個方法調用都被外部同步包圍,該類對於併發使用也是不安全的。線程對立一般是因爲在不一樣步的狀況下修改靜態數據而致使的。沒有人故意編寫線程對立類;此類一般是因爲沒有考慮併發性而致使的。當發現類或方法與線程不相容時,一般將其修正或棄用。條目 78中的generateSerialNumber方法在沒有內部同步的狀況下是線程對立的,如第322頁所述。code

這些分類(除了線程對立)大體對應於《Java Concurrency in Practice》一書中的線程安全註解,分別是Immutable,ThreadSafe和NotThreadSafe [Goetz06,附錄A]。 上述分類中的無條件和條件線程安全類別都包含在ThreadSafe註解中。對象

在文檔記錄了一個有條件的線程安全類須要當心。 你必須指明哪些調用序列須要外部同步,以及必須獲取哪一個鎖(或在極少數狀況下是幾把鎖)才能執行這些序列。 一般是實例自己的鎖,但也有例外。 例如,Collections.synchronizedMap的文檔說明了這一點:

It is imperative that the user manually synchronize on the returned map when iterating over any of its collection views:
當迭代任何Map集合的視圖時,用戶必須手動同步返回的Map:

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();
}

不遵循此建議可能會致使不肯定性行爲。

類的線程安全性的描述一般屬於類的文檔註釋,但具備特殊線程安全屬性的方法應在其本身的文檔註釋中描述這些屬性。 沒有必要記錄枚舉類型的不變性。 除非從返回類型中顯而易見,不然靜態工廠必須在文檔中記錄返回對象的線程安全性,如Collections.synchronizedMap(上文)所示。

當類承諾使用可公開訪問的鎖時,它容許客戶端以原子方式執行一系列方法調用,但這種靈活性須要付出代價。 它與併發集合(如ConcurrentHashMap)使用的高性能內部併發控制不兼容。 此外,客戶端能夠經過長時間保持可公開訪問的鎖來發起拒絕服務攻擊。 這多是偶然也多是故意的。

要防止此拒絕服務攻擊,可使用私有鎖對象而不是使用synchronized方法(這隱含着可公開訪問的鎖):

// Private lock object idiom - thwarts denial-of-service attack
private final Object lock = new Object();

public void foo() {
    synchronized(lock) {
        ...
    }
}

因爲私有鎖對象在類外是不可訪問的,所以客戶端不可能干擾對象的同步。 實際上,咱們經過將鎖定對象封裝在它同步的對象中來應用條目 15的建議。

請注意,鎖定屬性(lock field)被聲明爲final。 這能夠防止無心中更改其內容,從而致使災難性的非同步訪問(條目 78)。 咱們經過最小化鎖定屬性(lock field)的可變性來應用條目 17的建議。 鎖定屬性(lock field)應始終聲明爲final。 不管使用普通的監視器鎖(如上所示)仍是使用java.util.concurrent.locks包中的鎖,都是如此。

私有鎖對象習慣用法只能用於無條件線程安全類。 有條件線程安全類不能使用這個習慣用法,由於它們必須文檔記錄在執行某些方法調用序列時客戶端要獲取的鎖。

私有鎖對象習慣用法特別適合用於爲繼承設計的類(條目 19)。 若是這樣的類要使用其實例進行鎖定,則子類可能容易且無心地干擾基類的操做,反之亦然。 經過爲不一樣的目的使用相同的鎖,子類和基類可能最終「踩到彼此的腳趾。」這不只僅是一個理論問題;它就發生在Thread類上[Bloch05,Puzzle 77]中。

總之,每一個類都應該用措辭嚴謹的描述或線程安全註解清楚地文檔記錄其線程安全屬性。synchronized修飾符在本文檔中沒有任何做用。條件線程安全類必須文檔記錄哪些方法調用序列須要外部同步,以及在執行這些序列時須要獲取哪些鎖。若是你編寫一個無條件線程安全的類,請考慮使用一個私有鎖對象來代替同步方法。這將保護免受客戶端和子類的同步干擾,並提供更大的靈活性,以便在後續的版本中採用複雜的併發控制方法。

相關文章
相關標籤/搜索