Java併發編程學習——《Java Concurrency in Practice》學習筆記 4.對象的組合

4.1 設計線程安全的類

通過使用封裝技術,可以使得在不對整個程序進行分析的情況下就可以判斷一個類是否是線程安全的。

再設計線程安全類的過程中,要包含的三個基本要素:
- 找出構成對象狀態的所有變量
- 找出約束狀態變量的不變性條件
- 建立對象狀態的併發訪問管理策略

分析對象的狀態時,首先從對象的域開始。如果對象中所有的域都是基本類型的變量,那麼這些域將構成對象的全部狀態。如果在對象的域中引用了其他對象,那麼該對象的狀態將包含被引用對象的域。

同步策略 Synchronization Policy
同步策略定義瞭如何在不違背對象不變條件或後驗條件的情況下對其狀態的訪問操作進行協同。規定了如何將不可變性、線程封閉域加鎖機制等結合起來以維護線程的安全性,並且還規定了那些變量由那些鎖來保護。要確保開發人員可以對這個類進行分析與維護,就必須將同步策略寫爲正式文檔。

4.1.1 收集同步需求

要確保類的線程安全性,就需要確保它的不變性條件不會再併發訪問的情況下被破壞。對象與變量所有可能的取值稱作狀態空間,狀態空間越小,判斷線程的狀態就越容易。final域越多,就越能簡化對象可能狀態的分析過程。

如果對象的下一個狀態依賴於當前的狀態,狀態更新操作就必須是一個複合操作。並非所有的操作都會在狀態轉換上施加限制。

狀態轉換的過程中如果存在無效的狀態轉換,那麼該操作必須是原子的。

4.1.2 依賴狀態的操作

如果在某個操作中包含有基於狀態的 先驗條件 Precondition ,那麼這個操作就稱爲依賴狀態的操作。

要想實現某個等待先驗條件爲真時才執行的操作,較簡便的方法是通過現有庫中的類,如 阻塞隊列 Blocking Queue 或 信號量 Semaphore

4.1.3 狀態的所有權

在定義哪些變量將構成對象的狀態時,只考慮對象擁有的數據。

許多情況下,所有權與封裝性是相關聯的。狀態變量的所有者決定採用何種加鎖協議來維持變量狀態的完整性。所有權意味着控制權。然而,如果發佈了某個可變對象的引用,那麼就不再擁有獨佔的控制權,最多是“共享控制權”。對於從構造函數或者從方法中傳遞進來的對象,類通常並不擁有這些對象,除非這些方法是被專門設計爲轉義傳遞進來的對象的所有權。

容器類通常表現出一種“所有權分離”的形式,其中容器類擁有其自身的狀態,而客戶端代碼則擁有容器中各個對象的狀態。

4.2 實例封閉

封裝簡化了線程安全類的實現過程,它提供了一種 實例封閉機制 Instance Confinement。當一個對象被封裝到另一個對象中時,能夠訪問被封裝對象的所有代碼路徑都是一致的。通過將封閉機制與合適的加鎖策略結合起來,可以確保以線程安全的方式來使用非線程安全的對象。

將數據封裝在對象內部,可以將數據的訪問限制在對象的方法上,從而更容易起確保線程在訪問數據時總能持有正確的鎖。

被封閉的對象一定不能超出它們既定的作用域。

使用實例封閉確保線程安全的例子

public class PersonSet {

    private final Set<Person> mySet = new HashSet<Person>();

    public synchronized void addPerson(Person p) {
        mySet.add(p);
    }

    public synchronized boolean containsPerson(Person p) {
        return mySet.contains(p);
    }

}

*如果Person是可變的,那麼在訪問從PersonSet中獲得的Person對象時,還需要額外的同步。

完整代碼

實例封閉是構件線程安全類的一個最簡單方式

實例封閉還使得不同的變量可以由不同的鎖來保護。

4.2.1 Java監視器模式

從線程封閉原則及其邏輯推論可以得出Java監視器模式。遵循Java監視器模式的對象會把對象的所有可變狀態都封裝起來,並由對象自己的內置鎖來保護。

監視器模式的典型示例

public class Counter {

    private long value = 0;

    public synchronized long getValue() {
        return value;
    }

    public synchronized long increament() {
        if (value == Long.MAX_VALUE) {
            throw new IllegalStateException("counter overflow");
        }
        return ++value;
    }

}

完整代碼

監視器模式知識一種編寫代碼的約定,對於任何一種鎖對象,只要自始至終都使用該鎖對象,都可以用來保護對象的狀態。

使用私有鎖保護對象狀態

public class PrivateLock {

    private final Object myLock = new Object();

    Widget widget;

    void someMethod() {
        synchronized (myLock) {
            // 訪問或修改Widget的狀態

        }
    }

}

完整代碼

使用私有的鎖對象而不是對象的內置鎖,或任何其他可通過共有方式訪問的鎖,有更多的額優點。因爲鎖被封裝了,客戶無法得到。不會產生活躍性問題。

4.3 線程安全的委託

在某些情況下,通過多個線程安全類組合而成的類是線程安全的,但在某些情況下不是。

4.3.2 獨立的狀態變量

可以將線程安全性委託給多個狀態變量,只要這些變量是彼此獨立的,即組合而成的類並不會在其包含的多個狀態變量上增加任何不變條件。

4.3.3 多個狀態變量

如果某個類含有複合操作,那麼僅靠委託並不足以實現線程安全性。在這種情況下,這個類必須提供自己的加鎖機制以保證這些複合操作都是原子操作,除非整個複合操作都可以委託給狀態變量。

如果一個類是由多個獨立且線程安全的狀態變量組成,並且在所有的操作中都不包含無效狀態轉換,那麼可以將線程安全性委託給底層的狀態變量。

4.3.4 發佈底層的狀態變量

當把線程安全性委託給某個對象的底層狀態變量時,在什麼條件下可以發佈這些變量從而使其他類能夠修改它們,取決於在類中對這些變量施加了哪些不變性條件。

如果一個狀態變量是線程安全的,並且沒有任何不變性條件來約束它的值,在變量的操作上也不存在任何不允許的狀態轉換,那麼就可以安全的發佈這個變量。

4.4 在現有的線程安全類中添加功能

要添加一個新的原子操作,最安全的方法是修改原始類,單着通常無法做到。另一種方法是擴展這個類,假定在設計這個類時考慮了可擴展性。“擴展”方法比直接將代碼添加到類中更加脆弱,因爲同步策略實現被分佈到多個單獨維護的源代碼文件中。底層類同步策略的改變會導致子類被破壞。

4.4.1 客戶端加鎖機制

客戶端加鎖是指,對於使用某個對象X的客戶端代碼,使用X本身用於保護其狀態的鎖來保護這段客戶代碼。要使用客戶端加鎖,必須知道對象X使用的是哪一個鎖。

通過添加一個原子操作來擴展類是脆弱的,客戶端加鎖更加脆弱。因爲它將類C的加鎖代碼放到類C完全無關的其他類中。當在哪些並不承諾遵循加鎖策略的額類上使用客戶端加鎖時,要特別小心。

4.4.2 組合

組合 Composite 是爲現有類添加原子操作的更好的方法。

public class ImprovedList<T> implements List<T> {

    private final List<T> list;

    public ImprovedList(List<T> list) {
        this.list = list;
    }

    public synchronized boolean putIfAbsent(T x) {
        boolean contains = list.contains(x);
        if (contains) {
            list.add(x);
        }
        return !contains;
    }

    @Override
    public synchronized boolean add(T arg0) {
        return list.add(arg0);
    }

    // ...
}

完整代碼

4.5 將同步策略文檔化

維護線程安全性時,文檔時最強大的工具之一。在文檔中應當說明客戶代碼需要了解的線程安全性保證,以及代碼維護人員需要了解的同步策略

應該保證將類中的線程安全性文檔化。
- 是否線程安全
- 執行回調時是否持有一個鎖
- 是否有某些特定的鎖會影響其行爲
- 可以不指向支持客戶端加鎖,但要明確指出
- 如果希望客戶代碼能夠在類中添加新的原子操作,那麼需要在文檔說明需要獲得哪些鎖才能實現安全的原子操作
- 如果使用鎖來保護狀態,要將其寫入文檔以便日後維護

如果某個類沒有明確的聲明是線程安全的,那麼就不要假設它是線程安全的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章