1.設計線程安全的類
在設計線程安全類的過程中,需要包含以下三個基本元素:
- 找出構成對象狀態的所有變量。
- 找出約束狀態變量的不變性條件。
- 建立對象狀態的併發訪問管理策略。
同步策略(Synchronization Policy)
同步策略定義瞭如何在不違背對象不變性或後驗條件的情況下對其狀態的訪問操作進行協同。同步策略規定了如何將不可變性、線程封閉與加鎖機制等結合起來以維護線程的安全性,並且還規定了哪些變量由哪些鎖保護。
收集同步需求
- 找出用於判斷狀態的有效性的不可變條件。
- 確定用於判斷狀態遷移的有效性的後驗條件。
- 由於不變性條件以及後驗條件在狀態及狀態轉換上施加的各種約束,因此需要額外的同步與封裝。
依賴狀態的操作
如果某個操作中包含基於狀態的先驗條件(Precondition),那麼這個操作就被稱爲依賴狀態的操作。
在單線程程序中,如果某個操作無法滿足先驗條件,那麼就只能失敗。但在併發程序中,先驗條件可能會由於其他線程執行的操作而變成真。在併發程序中要一直等到先驗條件爲真,然後再執行該操作。
狀態的所有權
許多情況下,所有權與封裝性總是相互關聯:對象封裝它擁有的狀態,反之,即對它封裝的狀態擁有所有權。
狀態變量的所有者將決定採用何種加鎖協議來維持變量狀態的完整性。所有權意味着控制權。然而,如果發佈了某個可變對象的引用,那麼不再擁有獨佔的控制權,最多是共享控制權。
對用從構造函數或者從方法中傳遞進來的對象,類通常並不擁有這些對象,除非這些方法是被專門設計爲轉移傳遞進來的對象的所有權(例如,Collections
類提供各種工廠方法)。容器類通常表現出一種所有權分離的形式,其中容器類擁有其自身的狀態,客戶端代碼則擁有容器中各個對象的狀態。
2.實例封閉
封裝簡化了線程安全類的實現過程,它提供了一種實例封閉機制(Instance Confinement),通常也稱爲“封閉”。將數據封裝在對象內部中,可以將數據的訪問限制在對象的方法上,從而更容易確保線程在訪問數據時總能持有正確的鎖,同時更容易對代碼進行分析。
被封閉對象一定不能超出它們即定的作用域。
Java監視器模式
Java的內置鎖被稱爲監視器鎖或監視器。
Java監視器模式僅僅是一種編寫代碼的約定,對於任何一種鎖對象,只要自始至終都使用該鎖對象,都可以用來保護對象的狀態。
使用私有的鎖而不是對象的內置鎖(或任何其他可通過公有方式訪問的鎖)的優點:
- 私有的鎖對象可以將鎖封裝起來,使客戶代碼無法得到鎖。
- 如果客戶代碼錯誤的獲得了另一個對象的鎖,那麼可能會產生活躍性問題。
- 如果要驗證某個公有訪問的鎖在程序中是否被正確地使用,則需要檢查整個程序,而不是單個的類。
示例:車輛追蹤
這個示例將在這一章中經過多次改造。
一個用於調度車輛的“車輛追蹤器”,每臺車都由一個String
對象來標識,並且擁有一個相應的位置座標(x, y)
。在VehicleTracker
類中封裝了車輛標識和位置,因而它非常適合作爲基於MVC模式的GUI應用程序中的數據模型,並且該模型將由一個視圖線程和多個執行更新操作的線程共享。
視圖線程會讀取車輛的名字和位置,並將它們顯示在界面上:
Map<String, Point> locations = vehicles.getLocations();
for (String key : locations.keySet()) {
renderVehicle(jet, locations.get(key));
}
執行更新操作的線程:
void vehicleMoved(VehicleMovedEvent evt) {
Point loc = evt.getNewLocation();
vehicles.setLocation(evt.getVehicleId(), loc.x, loc.y);
}
基於監視器模式的車輛追蹤
@ThreadSafe
public class MonitorVehicleTracker {
@GuardeBy("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;
}
/**
* deepCopy並不只是用 unmodifiableMap 來包裝Map的,因爲這隻能防止容器對象被修改,而不能防止調用者修改保存在容器中的可變對象。
* 基於同樣的原因,如果只是通過拷貝構造函數來填充deepCopy中的HashMap,那麼同樣是不正確的。
* 因爲這樣做只複製了執行Point對象的引用,而不是Point對象本身。
*
*/
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);
}
}
@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;
}
}
在某種程度上,這種實現方式是通過早返回客戶代碼之前複製可變的數據來維持線程安全性的。通常情況下,這並不存在性能問題,但在車輛容器非常大的情況下將極大地降低性能。此外,由於每次調用getLocation()
就要複製數據,因此將出現一種錯誤情況——雖然車輛的實際位置發生了變化,但返回的信息卻保持不變。這種情況是好是壞,要取決於需求。如果在loction
集合上存在內部一致性需求,那麼這就是優點,在這種情況下返回一致的快照就非常重要,然而,如果調用者需要每輛車的最新信息,那麼就是確定,因爲這需要非常頻繁地刷新快照。
3.線程安全性的委託
大多數對象都是組合對象。如果使用多個線程安全類來組合成的類,在某些情況下是線程安全的,例如只有一個狀態的計數器程序或者多個狀態變量是彼此獨立的,即組合而成的類並不會在其包含的多個狀態變量上增加任何不變性條件;在其他一些情況下,這僅僅是一個好的開始。
示例:基於委託的車輛追蹤器
@ThreadSafe
public class DelegatingVehicleTracker {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String, Point> points) {
this.locations = new ConcurrentHashMap<String, Point>(points);
this.unmodifiableMap = Collections.unmodifiableMap(this.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("No such ID:" + id);
}
}
/**
* Point類是不可變的,因而它是線程安全的。所以在進行操作的時候不需要進行復制。
*
*/
@Immutable
public class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
在當前的車輛追蹤類中,返回的是一個不可變但卻實時的車輛位置視圖。如果需要一個不發生變化的車輛視圖:
public Map<String, Point> getLocations() {
return Collections.unmodifiableMap(new HashMap<String, Point>(this.locations));
}
委託失效
如果多個狀態變量之間存在着某些不變性條件,那麼依靠委託給線程安全類是不足以保護它的不變性條件。如果某個類含有複合條件,那麼依靠委託並不足以實現線程安全性。在這種情況下,這個類必須提供自己的加鎖機制以保證這些複合操作都是原子操作,除非整個複合操作都可以委託給狀態變量。
如果一個類是由多個獨立且線程安全的狀態變量組成,並且在所有的操作中都不包含無效狀態轉換,那麼可以將線程安全性委託給底層的狀態變量。
發佈底層的狀態變量
如果一個狀態變量是線程安全的,並且沒有任何不變性條件來約束它的值,在變量的操作上也不存在任何不允許的狀態轉換,那麼就可以安全地發佈這個變量。
示例:發佈狀態的車輛追蹤器
@ThreadSafe
public class PublishingVehicleTracker {
private final Map<String, SafePoint> locations;
private final Map<String, SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String, SafePoint> SafePoints) {
this.locations = new ConcurrentHashMap<String, SafePoint>(SafePoints);
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("No such ID:" + id);
locations.get(id).set(x, y);
}
}
@ThreadSafe
public class SafePoint {
@GuardedBy("this")
private int x, y;
private SafePoint(int[] a) {
this(a[0], a[1]);
}
/**
* 如果在拷貝構造韓式實現爲this(p.x, p.y),那麼會產生競態條件
*/
public SafePoint(SafePoint p) {
this(p.get());
}
public SafePoint(int x, int y) {
this.x = x;
this.y = y;
}
/**
* 這裏沒有未x、y分別提供get方法,爲了避免在獲得這兩個不同座標的操作之間,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;
}
}
PublishingVehicleTracker
是線程安全的,但如果它在車輛位置的有效值上施加了任何約束,那麼就不再是線程安全的。如果需要對車輛位置的變化進行判斷或者當位置變化時執行一些操作,那麼PublishingVehicleTracker
中採用的方法並不合適。
4.在現有的線程安全類中添加功能
擴展目標類
如果這個類在設計時考慮了可擴展性,那麼可以去擴展這個類來達到增加功能的目的。
// 擴展Vector並增加一個“若沒有則添加”方法
@ThreadSafe
public class BetterVetor<E> extends Vector<E> {
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if (absent)
add(x);
return absent;
}
}
“擴展”方法比直接將代碼添加到類中更加脆弱,因爲將同步策略實現分佈到多個單獨維護的源代碼文件中。如果底層的類改變了同步策略並選擇了不同的鎖來保護它的狀態變量,那麼子類會被破壞,因爲在同步策略改變後它無法再使用正確的鎖來控制對基類狀態的併發訪問。
客戶端加鎖機制
對於使用某個對象X的客戶端代碼,使用X本身用於保護其狀態的鎖來保護這段客戶代碼。要使用客戶端加鎖,必須要知道對象X使用的哪一個鎖。
// 通過客戶端加鎖來實現“若沒有則添加”
@ThreadSafe
public class ListHelper<E> {
public List<E> list = Collections.synchtonizedList(new ArrayList<E>());
public boolean putIfAbsent(E x) {
synchronized(list) {
boolean absent = !contains(x);
if (absent)
add(x);
return absent;
}
}
}
客戶端加鎖卻更加脆弱,因爲它將類C的加鎖代碼放到與C完全無關的其他類中。當在那些並不承諾遵循加鎖策略的類上使用客戶端加鎖時,要特別小心。
組合
當爲現有的類添加一個原子操作時,可以使用組合去實現,例如採用包裝器模式來增加某個類的功能。
// 通過組合實現“若沒有則添加”
@ThreadSafe
public class ImprovedList<T> implements List<T> {
private final List<T> list;
public ImprovedList(List<T> list) { this.list = list; }
public synchronized boolean putIfAbsent(E x) {
boolean contains = list.contains(x);
if (!contains)
list.add(x);
return !contains;
}
public synchronized void clear() { list.clear(); }
// ... 按照類似的方法委託List的其他方法
}
ImprovedList
通過自身的內置鎖增加了一層額外的加鎖,ImprovedList
不關心底層的List
是否是線程安全的,它都會通過一致的加鎖機制來實現線程安全性。組合方式的加鎖實現會比之前兩種更爲健壯。