1、前言
書中在解釋Java監視器模式的時候使用了一個車輛追蹤器例子,根據不同的使用場景給出了不同的實現和優化。
2、監視器模式示例
實現一個調度車輛的“車輛追蹤器”,每台車使用一個String對象標識,並且擁有一個相應的位置坐標(x,y)。由於運行在多線程的場景下,對外暴露的接口需要保證線程安全。
需要提供的接口包括:
- 獲取所有車輛標識和位置
- 讀取某個車輛位置
- 更新某個車輛位置
下面給出第一種實現:
@ThreadSafe public class MonitorVehicleTracker { @GuardedBy("this") private final Map<String, MutablePoint> locations; public MonitorVehicleTracker(Map<String, MutablePoint> points) { locations = deepCopy(points); } public synchronized Map<String, MutablePoint> getLocations() { return deepCopy(locations); } public synchronized MutablePoint getLocation(String key) { MutablePoint point = locations.get(key); return point == null ? null : new MutablePoint(point); } public synchronized void setLocation(String id, int x, int y) { if (id == null) { return; } MutablePoint point = locations.get(id); if (point == null) { throw new IllegalArgumentException("No such ID: " + id); } point.setPoint(x, y); } private Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> points) { if (points == null) { return Maps.newHashMap(); } Map<String, MutablePoint> result = Maps.newHashMapWithExpectedSize(points.size()); for (String key : points.keySet()) { result.put(key, new MutablePoint(points.get(key))); } return Collections.unmodifiableMap(result); } } @NotThreadSafe public class MutablePoint { private int x, y; public MutablePoint(int x, int y) { this.x = x; this.y = y; } public MutablePoint() { } public MutablePoint(MutablePoint point) { if (point == null) { throw new IllegalArgumentException("param is null"); } int[] pointArray = point.getPointArray(); x = pointArray[0]; y = pointArray[1]; } public int[] getPointArray() { int[] ret = new int[2]; ret[0] = x; ret[1] = y; return ret; } public void setPoint(int x, int y) { this.x = x; this.y = y; } }
首先需要定義一個表示位置的類MutablePoint,該類是可變的,非線程安全的。車輛追蹤器的邏輯實現在類MonitorVehicleTracker,提供了所需的三種接口邏輯。MonitorVehicleTracker是一個線程安全類,通過java內置鎖(synchronized)和深度拷貝實現,返回的位置信息拷貝了當前的數據,包括車輛表示和對應的位置信息。這種實現方式得到的位置信息是當前的快照,這樣的數據結果是否合適取決於你的需求。
上面這個實現使用了深度拷貝的方式,這種方式在車輛數量非常大的時候存在性能問題。那么是否可以直接返回原有的數據呢,答案是不可以,如果直接返回,這樣意味着直接發布了不支持線程安全的HashMap結構,該數據會在多個線程將共享。
那么我們是否有其他方式解決這個問題呢。一種方案是將線程安全的能力委托給類中內部組件,而java提供了線程安全的HashMap-concurrentHashMap(HashTable、Collections.synchronizedMap()性能不及ConcurrentHashMap)
下面給出第二種實現:
@ThreadSafe public class MonitorVehicleTracker { private final ConcurrentHashMap<String, ImmutablePoint> locations; private final Map<String, ImmutablePoint> unmodifiedLocations; public MonitorVehicleTracker(Map<String, ImmutablePoint> pointMap) { locations = new ConcurrentHashMap<>(pointMap); unmodifiedLocations = Collections.unmodifiableMap(locations); } public Map<String, ImmutablePoint> getLocations() { return unmodifiedLocations; } public void setLocation(String id, int x, int y) { if (StringUtils.isBlank(id)) { return; } if (locations.replace(id, new ImmutablePoint(x, y)) == null) { throw new IllegalArgumentException("No such ID: " + id); } } public ImmutablePoint getLocation(String id) { if (StringUtils.isBlank(id)) { throw new IllegalArgumentException("param is null"); } return locations.get(id); } } @Immutable @ThreadSafe public class ImmutablePoint { private final int x, y; public ImmutablePoint(int x, int y) { this.x = x; this.y = y; } public ImmutablePoint(MutablePoint point) { if (point == null) { throw new IllegalArgumentException("param is null"); } int[] pointArray = point.getPointArray(); x = pointArray[0]; y = pointArray[1]; } public int[] getPointArray() { int[] ret = new int[2]; ret[0] = x; ret[1] = y; return ret; } }
第二個實現中,MonitorVehicleTracker類的線程安全能力委托給內部組件。因為ConcurrentHashMap本身是一個線程安全的HashMap,所以無需進行深度拷貝,直接在線程間共享該數據結構即可。從上面的實現可以看到,位置信息使用ImmutablePoint而不是MutablePoint,這是因為位置信息也會發布出去,也可能會在線程間共享,而ConcurrentHashMap只能保證自身操作的線程安全。ConcurrentHashMap的key、value都需要是線程安全的,ImmutablePoint使用不變性提供了線程安全,String可以認為是常量,同樣支持線程安全。與第一種實現發放不同的是,每個線程拿到的位置信息視圖是一個變化的,並非快照,如果需要快照,通過淺拷貝即可實現。
實現一個線程安全的位置信息類還可以通過內置鎖實現,同樣,整個MonitorVehicleTracker類還是線程安全的。
上面這個實現通過委托給支持線程安全的內部組件實現線程安全,那么是不是只要內部組件是線程安全的那這個類就是線程安全的呢,顯然不是的,如果內部組件的數據存在邏輯關系,或者存在復合操作時,線程安全需要滿足既定的邏輯關系,保證符合操作的原子性,這些都是需要額外的同步操作來完成
在擴展原有支持線程安全類的時候,不管是通過繼承方式還是組合方式(客戶端加鎖),都需要保證擴展類中加的鎖和基類的鎖是一個鎖。