《Java併發編程實戰》第四章筆記

對象的組合

設計線程安全的類

  • 在設計線程安全類的過程中,需要包含以下三個基本要素:
    • 找出構成對象狀態的所有變量
    • 找出約束狀態變量的不變性條件
    • 建立對象狀態的併發訪問管理策略
// 使用Java監視器模式的線程安全計數器 

@ThreadSafe
public final class Counter {
	@GuardedBy("this") private long value = 0;
	
	public synchronized long getValue () {
		return value;
	}
	public synchronized long increment () {
		if (value == Long.MAX_VALUE) {
			throw new IllegalStateException("counter overflow");	
		}
		return ++value;
	}
}
  • 同步策略(Synchronization Policy)定義瞭如何在不違背對象不變條件或後驗條件的情況下對其狀態的訪向操作進行協同。同步策略規定了如何將不可變性、線程封閉與加鎖機制等結含起來以維護線程的安全性,並且還規定了那些變量由哪些鎖來保護.要確保開發人員可以對這個類進行分析與維護,就必須將同步策略寫爲正式文檔。

收集同步需求

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

依賴狀態的操作

狀態的所有權

  • 在定義哪些變量將構成對象狀態時,只考慮對象擁有的數據。

實例封閉

  • 如果某對象不是線程安全的,那麼可以通過多種技術使其在多線程程序中安全地使用。你可以確保該對象只能由單個線程訪問(線程封閉),或者通過一個鎖來保護對該對象的所有訪問。
  • 封裝簡化了線程安全類的實現過程,它提供了一種實例封閉機制(Instance Confinement),通常也簡稱爲“封閉”。
  • 將數據封裝在對象內部,可以將數據的訪問限制在對象的方法上,從而更容易確保線程在訪問數據時總能持有正確的鎖。
// 通過封閉機制來確保線程安全

@ThreadSafe
public class PersonSet {
	@GuardedBy("this")
	private final Set<Person> mySet = new HashSet<Person>();
	
	public synchronized void addPerson (Person p) {
		mySet.add(p);
	}
	
	public synchronized  boolean containsPerson (Person p) {
		return mySet.contains(p);
	}
}
  • 實例封閉式構建線程安全類的一個最簡單方式,它還使得在鎖策略的選擇上擁有了更多的靈活性。
  • 封閉機制更易於構造線程安全的類,因爲當封閉類的狀態時,在分析類的線程安全性時就無須檢查整個程序。

Java監視器模式

  • 從線程封閉原則及其邏輯推論可以得出Java監視器模式。遵循Java監視器模式的對象會把對象的所有可變狀態都封裝起來,並由對象自己的內置鎖來保護。
// 通過一個私有鎖來保護狀態

public class PrivateLock {
	private final Object myLock = new Object();
	@GuardedBy("myLock") Widget widget;
	
	void someMethod () {
		synchronized (myLock) {
			// 訪問或修改Widget的狀態
		}
	}
}
  • 使用私有的鎖對象而不是對象的內置鎖(或任何其他可通過公有方式訪問的鎖),有許多優點。私有的鎖對象可以將鎖封裝起來,使客戶代碼無法得到鎖,但客戶代碼可以通過公有方法來訪問鎖,以便(正確或者不正確地)參與到它的同步策略中,如果客戶代碼錯誤地獲得了另一個對象的鎖,那麼可能會產生活躍性問題。此外,要想驗證某個公有訪問的鎖在程序中是否被正確地使用,則需要檢査整個程序,而不是單個的類。

示例:車輛追蹤

// 基於監視器模式的車輛追蹤

@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.ketSet()) {
			result.put(id, new MutablePoint(m.get(id)));
		}
		return Collections.unmodifiableMap(result);
	}
}
// 與Java.awt.Point類似的可變Point類

@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;
	}
}

線程安全性委託

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

// 在 DelegatingVehicleTrcker 中使用的不可變Point類

@Immutable
public class Point {
	public final int x, y;
	public Point (int x, int y) {
		this.x = x;
		this.y = y;
	}
}
  • 由於Point類是不可變的,因而它是線程安全的。不可變的值可以被自由地共享與發佈,因此在返回location時不需要複製。
// 將線程安全委託給 ConcurrentHashMap

@ThreadSafe
public class DelegatingVehicleTracker {
	private final ConcurrentMap<String, Point> locations;
	private final Map<String, Point> unmodifiableMap;
	
	public DelegatingVehucleTracker (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);
		}
	}
}
// 返回 locations 的靜態拷貝而非實時拷貝

public Map<String, Point> getLocations () {
	return Collections.unmodifiableMap(new HashMap<String, Point>(locations));
}

獨立的狀態變量

// 將線程安全性委託給多個狀態變量

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);
	}	
}

當委託失效時

// NumberRange 類並不足以保護它的不變性條件

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()) {
			if (i > upper.get()) {
				throw new IllegalArgumentException("can't set lower to " + i + " > upper");
			}
			lower.set(i);
		}
	}
	
	public void setUpper (int i) {
		// 注意 --- 不安全的“先檢查後執行”
		if (i < lower.get()) {
			throw new IllegalArgumentException("can't set upper to " + i + " < lower");
		}
		upper.set(i);
	}
	
	public boolean isInRange (int i) {
		return(i >= lower.get() && i <= upper.get());
	}
}
  • 如果一個類是由多個獨立且線程安全的狀態變量組成,並且在所有的操作中都不包含無效狀態轉換,那麼可以將線程安全性委託給底層的狀態變量。

發佈底層的狀態變量

  • 如果一個狀態編程是線程安全的,並且沒有任何不變性條件來約束它的值,在變量的操作上也不存在任何不允許的狀態轉換,那麼就可以安全地發佈這個變量。

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

// 線程安全且可變的Point類

@ThreadSafe
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 synchronized int[] get () {
		return new int[] { x, y };
	}
	
	public synchronized void set (int x, int y) {
		this.x = x;
		this.y = y;
	}
}
// 安全發佈底層狀態的車輛追蹤器

@ThreadSafe
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);
	}
}

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

// 擴展Vector並增加一個“若沒有則添加”方法

@ThreadSafe
public class BetterVector<E> extends Vector<E> {
	public synchrnized boolean putIfAbsent (E x) {
		boolean absent = !contains(x);
		if (absent) {
			add(x)
		}
		return absent;
	}
}
  • “擴展”方法比直接將代碼添加到類中更加脆弱,因爲現在的同步策略實現被分佈到多個單獨維護的源代碼文件中。如果底層的類改變了同步策略並選擇了不同的鎖來保護它的狀態變量,那麼子類會被破壞,因爲在同步策略改變後它無法再使用正確的鎖來控制對基類狀態的併發訪問。(在Vector的規範中定義了它的同步策略,因此Bettervector不存在這個問題。)

客戶端加鎖機制

  • 對於由Collections.synchronizedList封裝的ArrayList,這兩種方法在原始類中添加一個方法或者對類進行擴展都行不通,因爲客戶代碼並不知道在同步封裝器工廠方法中返回的List對象類型。第三種策略是擴展類的功能,但並不是擴展類本身,而是將擴展代碼放入一個“輔助類”中。
// 非線程安全的“若沒有則添加”

@NotThreadSafe
public class ListHelper<E> {
	public List<E> list = 
		Collections.synchronizedList(new ArrayList<E>());
	
	public synchronized boolean putIfAbsent (E x) {
		boolean absent = !list.contains(x);
		if (absent) {
			list.add(x);
		}
		return absent;
	}
}
  • 爲什麼這種方式不能實現線程安全性?畢竟,putlfAbsent 已經聲明爲 synchronized 類型的變量,對不對?問題在於在錯誤的鎖上進行了同步。無論List使用哪一個鎖來保護它的狀態,可以確定的是,這個鎖並不是 ListHelper 上的鎖。ListHelper 是帶來了同步的假象,儘管所有的鏈表操作都被聲明爲synchronized,但卻使用了不同的鎖,這意味着 putlfAbsent 相對於 List 的其他操作來說並不是原子的,因此就無法確保當putlfAbsent執行時另一個線程不會修改鏈表。
// 通過客戶端加鎖來實現“若沒有則添加”

@ThreadSafe
public class ListHelper<E> {
	public List<E> list = 
		Collections.synchronizedList(new ArrayList<E>());
	
	pubic boolean putIfAbsent (E x) {
		synchronized (list) {
			boolean absent = !list.contains(x);
			if (absent) {
				list.add(x);
			}
			return absent;
		}
	}
}
  • 通過添加一個原子操作來擴展類是脆弱的,因爲它將類的加鎖代碼分佈到多個類中。然而,客戶端加鎖卻更加脆弱。因爲它將類C的加鎖代碼放到與C完全無關的其他類中。當在那些並不承諾遵循加鎖策略的類上使用客戶端加鎖時,要特別小心。
  • 客戶端加鎖機制與擴展類機制有許多共同點,二者都是將派生類的行爲與基類的實現耦合在一起。正如擴展會破壞實現的封裝性,客戶端加鎖同樣會破壞同步策略的封裝性。

組合

  • 當爲現有的類添加一個原子操作時,有一種更好的方法:組合。
// 通過組合實現“若沒有則添加”

@ThreadSafe
public class ImproveList<T> implements List<T> {
	private final List<T> list;
	
	public ImprovedList (List<T> list) { this.list = list; }
	
	public synchronized boolean putIfAbsent(T x) {
		boolean contains = list.contains(x);
		if (contains) {
			list.add(x);
		}
		return !contains;
	}
	
	public synchronized void clear () {
		list.clear();
	}
	// ...按照類似的方式委託List的其他方法
}

在這裏插入圖片描述

將同步策略文檔化

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