【Java併發.4】對象的組合

  到目前爲止,咱們已經介紹了關於線程安全與同步的一些基礎知識。然而,咱們並不但願對每一系內存訪問都進行分析以確保程序是線程安全的,而是但願將一些現有的線程安全組件組合爲更大規模的組件或程序。數組

4.1  設計線程安全的類安全

  經過使用封裝技術,可使得在不對整個程序進行分析的狀況下就能夠判斷一個類是不是線程安全的。多線程

  在設計線程安全類的過程當中,須要包含如下三個基本要素:
  • 找出構成對象狀態的全部變量。
  • 找出約束狀態變量的不變性條件。
  • 創建對象狀態的併發訪問管理策略。

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

  看以下清單:使用Java 監視器模式的線程安全計數器函數

public class Counter {
    private long value = 0;
    public synchronized long getValue() {
        return value;
    }
    public synchronized long increment() {
        if (value == Long.MAX_VALUE) {
            throw new IllegalArgumentException("");
        }
        return ++value;
    }
}

  同步策略(Synchronization Policy)定義瞭如何在不違背對象不變條件後驗條件的狀況下對其狀態的訪問操做進行協同。同步策略規定了如何將不可變性線程封閉加鎖機制等結合起來以維護線程的安全性,而且還規定了那些變量由那些鎖來保護。工具

 

4.1.1  收集同步需求性能

  要確保類的線程安全性,就須要確保它的不變性條件不會在併發訪問的狀況下被破壞,這就須要對其狀態進行推斷。一樣,在操做中還會包含一些後驗條件來判斷狀態遷移是否有效的。如自增值。this

  因爲不變性條件以及後驗條件在狀態及狀態轉換上施加了各類約束,所以就須要額外的同步與封裝。若是某些狀態是無效的,那麼必須對底層的狀態變量進行封裝,不然客戶代碼可能會使對象處於無效狀態。若是在某個操做中存在無效的狀態轉換,那麼該操做必須是原子的。另外,若是在類中沒有施加這種約束,那麼就能夠放寬封裝性或序列化等需求,以便得到更高的靈活性或性能。spa

若是不瞭解對象的不變性條件與後驗條件,那麼就不能確保線程安全性。要知足在狀態變量的有效值或狀態轉換上的各類約束條件,就須要藉助於原子性與封裝性。

 

4.1.2  依賴狀態的操做線程

  類的不變性條件與後驗條件約束了在對象上有哪些狀態和狀態轉換是有效的。若是在某個操做中包含有基於狀態的先驗條件,那麼這個操做就稱爲依賴的操做。

  等待某個條件爲真的各類內置機制(包括等待和通知等機制)都與內置加鎖機制緊密關聯,要想正確地使用它們並不容易。要想實現某個等待先驗條件爲真時才執行的操做,一種更簡單的方法是經過現有庫中的類(例如阻塞隊列【Blocking Queue】或信號量【Semaphore】)來實現依賴狀態的行爲。

 

4.2  實例封裝

  若是某對象不是線程安全的,那麼能夠經過多種技術使其在多線程程序中安全地使用。你能夠確保該對象只能由單個線程訪問(線程封閉),或者經過一個鎖來保護對該對象的全部訪問。

  封裝簡化了線程安全類的實現過程,它提供了一種實例封閉機制(instance Confienement)。當一個對象被封裝到另外一個對象中時,可以訪問被封裝對象的全部代碼路徑都是已知的。

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

  程序清單:經過封裝機制來確保線程安全

public class PersonSet {
    private final Set<Person> mySet = new HashSet<Person>();
    public synchronized void addPerson(Person p) {
        mySet.add(p);
    }
    public synchronized boolean containPerson(Person p) {
        return mySet.contains(p);
    }
}

  實例封裝是構建線程安全類的一個最簡單方式,它還使得在鎖策略的選擇上擁有了更多的靈活性。

  固然,若是將一個本該本封閉的對象發佈出去,那麼也會破壞封閉性。若是一個對象本應該封閉在特定的做用域內,那麼讓該對象逸出做用域就是一個做物。當發佈其餘對象時,例如迭代器或內部的類實例,可能會間接地發佈被封閉的對象,一樣會使本封閉的對象逸出。

封閉機制更容易構造線程安全的類,由於當類封閉的狀態時,在分析類的線程安全性時就無須檢查整個程序。

 

4.2.1  Java監視器模式

  從線程封閉原則及其邏輯推理能夠得出Java監視器模式。遵循Java監視器模式的對象會把對象的全部可變狀態都封裝起來,並由對象本身的內置鎖來保護。

  程序清單:經過一個私有鎖來保護狀態

public class PrivateLock {
    private final Object myLock = new Object();
    void someMethod() {
        synchronized (myLock) {
            //do something
        }
    }
}

  使用私有的鎖對象而不是對象的內置鎖(任何其餘可經過公有方式訪問的鎖),有許多優勢。私有的鎖對象能夠將鎖封裝起來,是客戶代碼沒法獲得鎖,但客戶代碼能夠經過公有方法來訪問,以便參與到它的同步策略中。若是客戶代碼錯誤地得到了另外一個對象的鎖,那麼可能會產生活躍性問題。此外,要想驗證某個公有訪問的鎖在程序中是否被正確地使用,則須要檢查整個程序,而不是單個的類。

 

4.2.2  示例:車輛追蹤

  如下程序清單中,咱們看一個示例: 一個用於調度車輛的「車輛追蹤器」。首先使用監視器模式來構建車輛追蹤器,而後嘗試放寬某些封裝性需求同時又保持線程安全性。

public class MonitorVehicleTracker {
    private final Map<String ,MutablePoint> locations;
    public MonitorVehicleTracker(Map<String ,MutablePoint> locations) {
        this.locations = deepCopy(locations);   //返回拷貝信息
    }

    public synchronized Map<String, MutablePoint> getLocations() {
        return deepCopy(locations); //返回拷貝信息
    }

    public synchronized MutablePoint getLocation(String id) {
        MutablePoint lo = locations.get(id);
        return lo == null ? null : new MutablePoint(lo);    //返回拷貝信息
    }

    public synchronized void setLocations(String id, int x, int y) {
        MutablePoint lo = locations.get(id);
        if (lo == null) {
            throw new IllegalArgumentException("");
        }
        lo.x = x;
        lo.y = y;
    }

    private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> locations) {
        Map<String, MutablePoint> result = new HashMap<String, MutablePoint>();
        for (String id : locations.keySet()) {
            result.put(id, new MutablePoint(locations.get(id)));
        }
        return Collections.unmodifiableMap(result);
    }
}
public class MutablePoint {        【不要這麼作public int x, y;
    public MutablePoint() {
        x = 0; y = 0;
    }
    public MutablePoint(MutablePoint p) {
        this.x = p.x;
        this.y = p.y;
    }
}

  雖然類 MutablePoint 不是線程安全的,但追蹤器類時線程安全的。它所包含的 Map 對象和可變的 Point 對象都不曾發佈。當須要返回車輛的位置時,經過 MutablePoint 拷貝構造函數或者 deepCopy 方法來複制正確的值,從而生成一個新的Map 對象,而且該對象中的值與原有 Map 對象中的 key 值和 value 值都相同。

  在某種程度上,這種實現方式是經過再返回客戶代碼以前複製可變的數據來維持線程安全性的。一般狀況下,這並不存在性能問題,但在車輛容器很是大的狀況下將極大地下降性能。

 

4.3  線程安全性的委託

4.3.1  示例:基於委託的車輛追蹤器

  下面將介紹一個更實際的委託示例,構造一個委託給線程安全類的車輛追蹤器。咱們將車輛位置保存到一個 實現線程安全的Map 對象中,還能夠用一個不可變的 Point 類來代替 MutablePoint 以保存位置。

  程序清單: 在DelegatingVehicleTracker 中使用的不可變 Point 類

public class Point {
    public final int x, y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

  因爲Point 類時不可變的,於是它是線程安全的。  將線程安全委託給 ConcurrentHashMap。

public class DelegatingVehicleTrack {
    private final ConcurrentMap<String, Point> locations;
    private final Map<String, Point> unmodifiableMap;
    public DelegatingVehicleTrack(Map<String, Point> pointMap) {
        locations = new ConcurrentHashMap<String, Point>(pointMap);
        unmodifiableMap = Collections.unmodifiableMap(locations);
    }
    public Map<String, Point> getLocations() {
        return unmodifiableMap;
    }
    public Point getLocation(String id) {
        return locations.get(id);
    }
    public void setLocations(String id, int x, int y) {
        if (locations.replace(id, new Point(x, y)) == null) {
            throw new IllegalArgumentException("");
        }
    }
}

  在使用監視器模式的車輛追蹤器中返回的是車輛位置的快照,而在使用委託的車輛追蹤器中返回的是一個不可修改但卻實時的車輛位置圖。

 

4.3.2  獨立的狀態變量

  到目前爲止,這些委託示例都僅僅委託給了單個線程安全的狀態變量。咱們還能夠將線程安全性委託給多個狀態變量,只要這些變量時彼此獨立的,即組合而成的類並不會再其包含的多個狀態變量上增長任何不變性條件。

  程序清單:將線程安全性委託給多個狀態變量

public class VisualComponent {
    private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<>();
    private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<>();
    public void addKeyListener(KeyListener keyListener) {
        keyListeners.add(keyListener);
    }
    public void addMouseListener(MouseListener mouseListener) {
        mouseListeners.add(mouseListener);
    }
    public void removeKeyListener(KeyListener keyListener) {
        keyListeners.remove(keyListener);
    }
    public void removeMouseListener(MouseListener mouseListener) {
        mouseListeners.remove(mouseListener);
    }
}

  VisualComponent 使用 CopyOnWriteArrayList 來保存各個監聽器列表。它是一個線程安全的鏈表,特別適用於管理監聽器列表。

 

4.3.3  當委託失敗時

  大多數組合對象都不會像 VisualComponent 這樣簡單:在它們的狀態變量之間存在着某些不變性條件。

  程序清單:NumbeRange 類並不足以保護它的不變性條件

public class NumberRange {        【不要這樣作//不變性條件 : lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);
    public void setLower(int i) {
        if (i > upper.get()) {  //  不安全的 先檢查後執行
            System.out.println("lower > upper");
            return;
        }
        lower.set(i);
    }
    public void setUpper(int i) {
        if (i < lower.get()) {  //  不安全的 先檢查後執行
            System.out.println("lower > upper");
            return;
        }
        upper.set(i);
    }
    public boolean isInRange(int i) {
        return (i >= lower.get() && i <= upper.get());
    }
}

  NumberRange 不是線程安全的,沒有維持對下界和上界進行約束的不變性條件。假設取值範圍在(0, 10),若是一個線程調用 setLower(5),而另外一個線程調用 setUpper(4),那麼在一些錯誤的執行時序中,這兩個調用都經過了檢查,而且都設置成功。所以,雖然 AtomicInteger 是線程安全的,但通過組合獲得的類卻不是線程安全的。

若是一個類是由多個獨立且線程安全的狀態變量組成,而且在全部的操做中都不包含無效狀態轉換,那麼能夠將線程安全性委託給底層的狀態變量。

 

4.3.4  發佈底層的狀態變量

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

若是一個狀態變量時線程安全的,而且沒有任何不變性條件來約束它的值,在變量的操做上也不存在任何不容許的狀態轉換,那麼久能夠安全地發佈這個變量。

 

4.3.5  示例:發佈狀態的車輛追蹤器

  咱們來構造車輛追蹤器的另外一個版本,並在這個版本中發佈底層的可變狀態。咱們須要修接口以適應這種變化,即便用可變且線程安全的 Point 類。

  程序清單:線程安全且可變的 Point 類

public class SafePoint {
    private int x, y;
    public SafePoint(SafePoint sp) {
        this.x = sp.x;
        this.y = sp.y;
    }
    private SafePoint(int[] a) {
        this(a[0], a[1]);
    }
    public SafePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public synchronized int[] get() {
        return new int[] {x, y};
    }
    public synchronized void set(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

  程序清單:安全發佈底層狀態的車輛追蹤器

public class PublishingVehicleTracker {
    private final Map<String, SafePoint> locations;
    private final Map<String, SafePoint> unmodifiableMap;
    public PublishingVehicleTracker(Map<String, SafePoint> locations) {
        this.locations = new ConcurrentHashMap<String, SafePoint>(locations);
        this.unmodifiableMap = Collections.unmodifiableMap(locations);
    }
    public Map<String, SafePoint> getLocations() {
        return unmodifiableMap;
    }
    public SafePoint getLocations(String id) {
        return locations.get(id);
    }
    public void setLocations(String id, int x, int y) {
        if (!locations.containsKey(id)) {
            throw new IllegalArgumentException("");
        }
        locations.get(id).set(x, y);
    }
}

 

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

  Java 類庫包含許多有用的「基礎模塊」類。一般,咱們應該優先選擇重用這些現有的類而不是建立新的類:重用能下降開發工做量、開發風險以及維護成本。有時候,某個現有的線程安全類能支持咱們須要的全部操做,但更多時候,現有的類智能支持大部分的操做,此時就須要在不破壞線程安全性的狀況下添加一個新操做。

  程序清單:擴展 Vector 並增長一個「若沒有則添加」方法

public class BetterVector<E> extends Vector {
    public synchronized boolean putIfAbsent(E e) {
        boolean absent = !contains(e);
        if (absent) add(e);
        return absent;
    }
}

  「擴展」方法比直接將代碼添加到類中更加脆弱,由於如今的同步策略實現被分佈到多個單獨維護的源代碼文件中。若是底層的類改變了同步策略並選擇了不一樣的鎖來保護它的狀態變量,那麼子類會被破壞,由於在同步策略改變後它沒法再使用正確的鎖來控制對基類狀態的併發訪問。

 

4.4.1  客戶端加鎖機制

  看一個錯誤例子:非線程安全的「若沒有則添加」

public class ListHelper<E> {        【不要這樣作public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    public synchronized boolean putIfAbsent(E e) {
        boolean absent = !list.contains(e);
        if (absent) list.add(e);
        return absent;
    }
}

  爲何這種方式不能實現線程安全性?畢竟,putIfAbsent 已經聲明爲 synchronized 類型的變量,對不對?問題在於在錯誤的鎖上進行了同步。不管List 使用哪個鎖來保護它的狀態,能夠肯定的是,這個鎖並非 ListHelper 上的鎖。

  要想使這個方法能正確執行,必須使List 在實現客戶端加鎖或外部加鎖時使用同一個鎖

  程序清單:經過客戶端加鎖來實現「若沒有則添加」

public class ListHelper<E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    public boolean putIfAbsent(E e) {
        synchronized (list) {
            boolean absent = !list.contains(e);
            if (absent) list.add(e);
            return absent;
        }
    }
}

 

4.4.2  組合

  當爲現有的類添加一個原子操做時,有一種更好的方法:組合(Composition)。看以下程序清單:經過組合實現「若沒有則添加」

public class ImprovedList<E> {
    private final List<E> list;
    public ImprovedList(List<E> list) {
        this.list = list;
    }
    public synchronized boolean putIfAbsent(E e) {
            boolean absent = !list.contains(e);
            if (absent) list.add(e);
            return absent;
    }
    public synchronized void clear() {
        list.clear();
    }
    // 按照相似的方式委託List的其餘方法
}

  ImprovedList 經過自身的內置鎖增長了一層額外的加鎖。

 

4.5  將同步策略文檔化

  在維護線程安全性時,文檔是最強大的(同時也是最未充分利用的)工具之一。

在文檔中說明客戶代碼須要瞭解的線程安全性保證,以及代碼維護人員須要瞭解的同步策略。
相關文章
相關標籤/搜索