【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  收集同步需求

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

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

如果不了解對象的不變性條件與后驗條件,那么就不能確保線程安全性。要滿足在狀態變量的有效值或狀態轉換上的各種約束條件,就需要借助於原子性與封裝性。

 

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  將同步策略文檔化

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

在文檔中說明客戶代碼需要了解的線程安全性保證,以及代碼維護人員需要了解的同步策略。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM