【Java併發編程實戰】04對象的組合

1. 設計線程安全的類

在設計線程安全類的過程中,需要包含以下三個基本要素:

  • 找出構成對象狀態的所有變量。
  • 找出約束變量的不變性條件。
  • 建立對象狀態的併發訪問管理策略。

1.1 收集同步需求

在很多類中都定義了一些不可變條件,用於判斷狀態是否有效。比如:計數器的取值範圍上存在一個限制,就是不能是負值。在操作中還會包含一些後驗條件來判斷狀態遷移是否有有效。比如:計數器的當前狀態爲8,那麼下一個有效狀態只能是9。由於不變性條件和後驗條件在狀態及狀態轉換上施加了各種約束,因此就需要額外的同步和封裝。

1.2 依賴狀態的操作

在某些對象的方法中還包含一些基於狀態的先驗條件。比如:不能從空隊列中移除一個元素,在刪除元素前,隊列必須處於非空狀態。如果在某個操作中包含有基於狀態的先驗條件,那麼這個操作就稱爲依賴狀態的操作。

1.3 狀態的所有權

對象封裝了它擁有的狀態,那麼它就擁有封裝狀態的所有權。狀態的所有者將決定採用何種加鎖協議來維持狀態的完整性。所有權意味着控制權。如果發佈了某個可變對象的引用,那麼就不再擁有獨佔的控制權,最多是共享控制權。爲了防止多個線程在併發訪問同一個對象時產生相互干擾,這些對象應該要麼是線程安全的對象,要麼是事實不可變的對象,或者用同一個鎖保護的對象。

2. 實例封閉

將數據封裝在對象內部,可以將數據的訪問限制在對象的方法上,從而更容易確保線程在訪問數據是總能持有正確的鎖。被封閉對象一定不能超過它們既定的作用域。對象可以封閉在類的一個實例中,比如:作爲類的一個私有成員;或者封閉在某個作用域內,比如:作爲一個局部變量;或者封閉在線程中,比如:在某個線程中將對象從一個方法傳遞到另一個方法,而不是在多個線程之間共享該對象。

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

2.1 Java監視器模式

遵循Java監視器模式的對象會把對象的所有可變狀態都封裝起來,並由對象自己的內置鎖來保護。示例:一個用於調度車輛的“車輛跟蹤器”,每臺車都由一個String對象來標識,並且擁有一個對應的位置座標(x,y)。創建一個追蹤器類用來封裝所有車輛的標識和位置,該類由多個線程(讀取操作和更新操作)共享。

@NotThreadSafe
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;
    }
}
@ThreadSafe
public class MonitorVehicleTracker {
    @GuardedBy("this") 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 loc = locations.get(id);
        return loc == null ? null : new MutablePoint(loc);
    }

    public synchronized void setLocation(String id, int x, int y) {
        MutablePoint loc = locations.get(id);
        if (loc == null)
            throw new IllegalArgumentException("No such ID: " + id);
        loc.x = x;
        loc.y = y;
    }

    private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) {
        Map<String, MutablePoint> result = new HashMap<String, MutablePoint>();

        for (String id : m.keySet())
            result.put(id, new MutablePoint(m.get(id)));

        return Collections.unmodifiableMap(result);
    }
}

雖然類MutablePoint不是線程安全的,但是追蹤器類是線程安全的。它所包含的Map對象和可變的Point對象都未曾發佈。當需要回去車輛的位置時,通過拷貝構造函數或deepCopy方法複製正確的值,從而生成一個新的Map對象。車輛少是,這並不存在性能問,單在車輛數量非常大的情況下將極大地降低性能。

3. 線程安全性的委託

大多數對象都是組合對象。當從頭開始構建一個類,或者將多個非線程安全的類組合爲一個類時,Java監視器模式是非常有用的。但是,如果類中的各個組件都已經是線程安全的,那麼實現線程安全就應視情況而定。

3.1 基於委託的車輛追蹤器

保存車輛位置信息的Map類,使用線程安全的ConcurrentMap類代替。使用不可變的Point類來代替MutablePoint類:

public class Point {
    public final int x, y;

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

在DelegatingVehicleTracker中沒有使用顯式的同步,所有對狀態的訪問都由ConcurrentHashMap來管理:

@ThreadSafe
public class DelegatingVehicleTracker {
    private final ConcurrentMap<String, Point> locations;
    private final Map<String, Point> unmodifiableMap;

    public DelegatingVehicleTracker(Map<String, Point> points) {
        locations = new ConcurrentHashMap<String, Point>(points);
        unmodifiableMap = Collections.unmodifiableMap(locations);
    }

    public Map<String, Point> getLocations() {
        return unmodifiableMap;
    }

    public Point getLocation(String id) {
        return locations.get(id);
    }

    public void setLocation(String id, int x, int y) {
        if (locations.replace(id, new Point(x, y)) == null)
            throw new IllegalArgumentException("invalid vehicle name: " + id);
    }
}

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

3.2 獨立的狀態變量

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

public class VisualComponent {
    private final List<KeyListener> keyListeners
            = new CopyOnWriteArrayList<KeyListener>();
    private final List<MouseListener> mouseListeners
            = new CopyOnWriteArrayList<MouseListener>();

    public void addKeyListener(KeyListener listener) {
        keyListeners.add(listener);
    }

    public void addMouseListener(MouseListener listener) {
        mouseListeners.add(listener);
    }

    public void removeKeyListener(KeyListener listener) {
        keyListeners.remove(listener);
    }

    public void removeMouseListener(MouseListener listener) {
        mouseListeners.remove(listener);
    }
}

VisualComponent類是一個圖形組件,允許客戶程序註冊監控鼠標和鍵盤等事件的監聽器。在鼠標事件監聽器和鍵盤事件監聽器之間不存在任何關聯,二者是彼此獨立的,因此VisualComponent可以將其線程安全性委託給這兩個線程安全的監聽器列表。

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

3.3 發佈底層的狀態變量

如果一個狀態變量是線程安全的,並且沒有任何不變性條件來約束它的值,在變量的操作上也不存在任何不允許的狀態轉換,那麼就可以安全地發佈這個變量。我們來修改之前的車輛追蹤器的代碼,使其發佈底層的可變狀態。首先寫一個可變並且線程安全的Point類:

public class SafePoint {
    @GuardedBy("this") private int x, y;

    private SafePoint(int[] a) {
        this(a[0], a[1]);
    }

    public SafePoint(SafePoint p) {
        this(p.get());
    }

    public SafePoint(int x, int y) {
        this.set(x, 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(this.locations);
    }

    public Map<String, SafePoint> getLocations() {
        return unmodifiableMap;
    }

    public SafePoint getLocation(String id) {
        return locations.get(id);
    }

    public void setLocation(String id, int x, int y) {
        if (!locations.containsKey(id))
            throw new IllegalArgumentException("invalid vehicle name: " + id);
        locations.get(id).set(x, y);
    }
}

在PublishingVehicleTracker中,其線程安全性委託給底層的ConcurrentHashMap,只是Map中的元素是線程安全的可變的。getLocations方法返回底層Map對象的一個不可變副本,調用者不能增加或刪除車輛,但可以通過修改返回Map中的SafePoint值來修改車輛的問題。

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