本章將介紹一些組合模式,這些模式能夠使一個類更容易成為安全執行緒的,並且在維護這些類時不會無意破壞類的安全性保證。
在單線程中,如果某個操作無法滿足先驗條件,那麼就只能是失敗,但在並發程式中,先驗條件可能會由於其他線程執行的操作而變成真。在並發程式中一直要等到先驗條件為真,然後再執行該操作。
在Java中,等待某個條件為真的各種內建機制(包括等待和通知機制)都與內建加鎖機制緊密關聯,要想正確的使用它們並不容易。要想實現某個等待先驗條件為真時才執行的操作,一種更簡單的方法是通過現有庫中的類(例如阻塞隊列[BlockingQueue]或訊號量[Semaphore])來實現依賴狀態的行為。
如果分配填充了一個HashMap對象,那麼就相當於填充了多個對象:HashMap對象,在HashMap對象中包含的多個對象,以及在Map.Entry中可能包含的內部對象。
執行個體封閉是構建安全執行緒類的一個最簡單方式。
車輛追蹤:
import java.util.Collections;import java.util.HashMap;import java.util.Map;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 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) {HashMap<String, MutablePoint> result = new HashMap<String, MutablePoint>();for(String id : m.keySet()) {result.put(id, new MutablePoint(m.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都相同。
public class Point {public final int x, y;public Point(int x, int y) {this.x = x;this.y = y;}}
import java.util.Collections;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;public class DelegatingVehicleTracker {private final ConcurrentHashMap<String, Point> locations;private final Map<String, Point> unmodifiableMap;public DelegatingVehicleTracker(Map<String, Point> points) {locations = new ConcurrentHashMap<>(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);}}}
如果一個狀態變數是安全執行緒的,並且沒有任何不變性條件來約束它的值,在變數的操作上也不存在任何不允許的狀態轉換,那麼就可以安全地發布這個變數。
在現有的安全執行緒類中添加功能
假如需要一個安全執行緒的列表,它需要提供一個原子的“若沒有則添加(put-if-absent)”的操作。同步的List類已經實現了大部分功能,我們可以根據它提供的contains方法和add方法來構造一個“若沒有則添加”的操作。
1.擴充Vector
@ThreadSafepublic class BetterVector<E> extends Vector<E>{public synchronized boolean putIfAbsent(E x) {boolean absent = !contains(x);if (absent) {add(x);}return absent;}}
2.通過用戶端加鎖來實現“若沒有則添加”
@ThreadSafepublic class ListHelper<E> {public List<E> list = Collections.synchronizedList(new ArrayList<E>());// ...public boolean putIfAbsent(E x) {synchronized (list) {boolean absent = !list.contains(x);if (absent) {list.add(x);}return absent;}}}
用戶端加鎖機制與擴充類機制有許多共同點,二者都是將衍生類別的行為與基類的實現耦合在一起。正如擴充會破壞實現的封裝性,用戶端加鎖同樣會破壞同步策略的封裝性。
3.通過組合實現“若沒有則添加”
當為現有的類添加一個原子操作時,有一種更好的方法:組合(Composition)。
@ThreadSafepublic 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 absent = !list.contains(x);if (absent) {list.add(x);}return absent;}public synchronized void Iclear() {list.clear();}// ... 按照類似的方式委託List的其他方法}
ImproveList通過自身的內建鎖增加了一層額外的加鎖。它並不關心底層的List是否是安全執行緒的,即使List不是安全執行緒的或者修改了它的加鎖實現,ImproveList也會提供一致的加鎖機制來實現線程的安全性。雖然額外的同步層可能導致輕微的效能損失,但與類比另一個對象的加鎖策略相比, ImprovedList更為健壯。
事實上我們使用了Java監視器模式來封裝現有的List,並且只要在類中擁有指向底層List的唯一外部參考,就能確保執行緒安全性。
同步策略文檔化
在文檔中說明用戶端代碼需要瞭解的執行緒安全性保證, 以及代碼維護人員需要瞭解的同步策略。
synchronized、volatile或者任何一個安全執行緒類都對應於某種同步策略,用於在並發訪問中確保資料的完整性。這種策略是程式設計的要素之一,因此應將其文檔化。
在設計同步策略時需要考慮多個方面
例如,將哪些變數聲明為volatile類型,哪些變數用鎖來保護,哪些鎖保護哪些變數,哪些變數必須是不可變的或者被封閉線上程中的,哪些操作必須是原子操作等。
其中某些方面是嚴格的實現細節,應該將它們文檔化以便於日後的維護。還有一些方面會影響類中加鎖行為的外在表現,也應該將其作為規範的一部分寫入文檔。
最起碼,應該保證將類中的執行緒安全性文檔化。它是否是安全執行緒的。在執行回調時是否持有一個鎖。 是否有某些特定的鎖會影響其行為。 不要讓用戶端冒著風險去猜測。
java.text.SimpleDateFormat並不是安全執行緒的。
如果某個類沒有明確地聲明是安全執行緒的,那麼就不要假設它是安全執行緒的。
從實現者的角度去解釋規範,而不是從使用者的角度去解釋。