4.2 实例封闭
4.2.2 示例:车辆追踪
程序清单 4-4 给出了一个基于 Java 监视器模式实现的“车辆追踪器”,其中使用了程序清单 4-5 中的 MutablePoint
来表示车辆位置。显然,更新车辆位置和获取车辆位置将在不同线程中进行,因此它们的读 / 写之间需要同步。
// 程序清单 4-4
@ThreadSafe
public class MonitorVehicleTracker {
@GuardBy("this")
private final Map<String, MutablePoint> locations;
public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
this.locations = deepCopy(locations);
}
// 通过 deepCopy 方法来深拷贝 locations 对象,并返回
public synchronized Map<String, MutablePoint> getLocations() {
return deepCopy(locations);
}
// 通过 MutablePoint 的拷贝构造函数来返回一个新的 MutablePoint 对象
public synchronized MutablePoint getLocation(String id) {
MutablePoint loc = locations.get(id);
return loc == null ? null : new MutablePoint(loc);
}
public synchronized void setLocations(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<>();
for (String id : m.keySet()) {
result.put(id, new MutablePoint(m.get(id)));
}
return Collections.unmodifiableMap(result);
}
}
// 程序清单 4-5
// 这个类并不是线程安全的,x, y 应申明为 final 类型,就是线程安全的
@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;
}
}
上面的 MutablePoint
不是线程安全的,但追踪器类是线程安全的。它所包含的 Map
对象和可变的 Point
对象都未曾发布。
- 当通过
getLocations
返回所有车辆位置时,通过deepCopy
方法来复制正确的值,从而生成一个新的Map
对象,并且该对象的值与原有Map
对象中的key
值和value
值都相同。 - 当通过
getLocation
返回特定车辆位置时,通过MutablePoint
的拷贝构造函数来复制正确的值,返回一个新生成的MutablePoint
对象。
程序清单 4-4 通过 synchronized
关键字来控制多线程的方法,通过复制可变的数据来维持线程安全性。通常情况下,这并不存在性能问题,但在车辆容器非常大的情况下将极大地降低性能①。此外,由于每次调用 getLocation
时就要复制数据,因此一种错误情况——线程 A 在获取车辆位置信息后,但是还未使用时,线程 B 更新了车辆位置,之后线程 A 使用车辆位置信息时却还是旧值。这种情况是好是坏,要取决于你的需求。如果在 location 集合上存在内部的一致性需求,那么这就是优点,在这种情况下返回一致的快照就非常重要。然而,如果调用者需要每辆车的最新信息,那么这就是缺点,因为这需要非常频繁地刷新快照。
① 由于
deepCopy
是从一个synchronized
方法中调用的,因此在执行时间较长的复制操作中, tracker 的内置锁将一直被占有,当有大量车辆需要追踪时,会严重降低用户界面的响应灵敏度。
4.3 线程安全性的委托
在上面 4.2 节中,我们的跟踪器类是由多个非线程安全的类组合而成,因此 Java 监视器模式(使用 synchronized
关键字,持有的锁是类内置锁,也就是监视器)就显得非常有用。但是,如果类中的各个组件都已经是线程安全的,我是否需要再增加一个额外的线程安全层?答案是,视情况而定。
4.3.1 示例:基于委托的车辆追踪器
下面我们将改造 MonitorVehicleTracker
类,构造一个委托给线程安全类的车辆追踪器。首先我们用一个不可变的 Point
类来代替 MutablePoint
以保存位置:
// 程序清单 4-6
// 由于有了 final 关键字,因此这是一个线程安全的不可变类
@Immutable
public class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
其次,我们在跟踪器类中使用线程安全的 ConcurrentHashMap
来代替 Map
类。
// 程序清单 4-7
@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<>(points); // 将传入进来的 Map 转换为线程安全的 ConcurrentHashMap
unmodifiableMap = Collections.unmodifiableMap(locations); // unmodifiableMap() 连浅拷贝都算不上,它只是 locations 引用的代理
}
public Map<String, Point> getLocations() {
return unmodifiableMap; // 由于 unmodifiableMap 只是 locations 的代理,而 locations 又是 ConcurrentHashMap 类型(具有可见性),因此 locations 的任何变化,外界都能及时观察到
}
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);
}
}
}
DelegatingVehhicleTracker
中没有使用任何显式的同步,所有对状态的访问都由 ConcurrentHashMap
来管理,而 ConcurrentHashMap
具有可见性的保证,而且 Map
所有的键和值都是不可变的。但是上面代码如果使用最初的 MutablePoint
类而不是 Point
类,就会破坏封装性,因为 getLocations
会发布一个指向可变状态的引用,而这个引用不是线程安全的。
我们稍微改变了车辆追踪器类的行为。在使用监视器模式的车辆追踪器中返回的是车辆位置的快照,而在使用委托的车辆追踪器中返回的是一个不可修改带却实时的车辆位置视图。这意味着,如果线程 A 调用 getLocations
,而线程 B 在随后修改了某些点的位置,那么在返回给线程 A 的 Map 中将反映这些变化。在 4.2 节中提到过,这可能是一种优点(更新的数据),也可能是一种缺点(可能导致不一致的车辆位置视图),具体情况取决于你的需求。
如果需要跟 4.2 节中行为一样,那么我们只需使 getLocations
方法返回一个视图的快照。要达到这个目的,那么 getLocations
可以返回对 locations
这个 Map
对象的一个浅拷贝(Shallow Copy,即生成一个新的 Map
,但是里面的每个 Entry
都是拷贝自原来的 Map
):
public Map<String, Point> getLocations() {
return Collections.unmodifiableMap(new HashMap<>(locations)); // HashMap 的拷贝构造函数是一次浅拷贝
}
上面返回的 unmodifiableMap
实际上是 locations
的一个浅拷贝。这样就保证了 getLocations
方法返回的是车辆位置信息的快照,因为 setLocation
方法中使用 replace
方法更新 locations
中的 Entry
,并不是直接更新的原 Entry
中的数据,而是新构造了一个 Entry
来替换 locations
中的原 Entry
。被替换掉的原 Entry
并不会被 gc,因为它的引用还存在 getLocations
返回的快照中,因此快照中的数据不会受 setLocation
方法更新数据的影响。
4.4 在现有的线程安全类中添加功能
Java 类库包含许多有用的“基础模块”类。通常,我们应该优先选择重用这些现有的类而不是创建新的类。但更多时候,现有的类只能支持大部分的操作,此时就需要在不破坏线程安全的情况下添加一个新的操作。
例如,假设需要一个线程安全的链表,它需要提供一个原子的“若没有则添加(Put-If-Absent)”的操作。同步的 List
类已经实现了大部分的功能,我们可以根据它提供的 contains
方法和 add
方法来构造一个“若没有则添加”的操作。
“若没有则添加”的概念很简单,在向容器中添加元素前,首先检查该元素是否已经存在,如果存在就不再添加。由于这个类必须是线程安全的,因此就隐含地增加了另一个需求,即“若没有则添加”这个操作必须是原子操作。
要添加一个新的原子操作,最安全的方法时修改原始的类,但这通常无法做到,因为你可能无法访问或修改类的源代码。
另一种方法是扩展这个类。程序清单 4-13 中的 BetterVector
对 Vector
进行了扩展,并添加了一个新方法 putIfAbsent
。扩展 Vector
很简单,但并非所有的类都像 Vector
那样将状态向子类公开,因此也就不是和采用这种方法。
// 程序清单 4-13
@ThreadSafe
public class BetterVector<E> extends Vector<E> {
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if (absent)
add(x);
return absent;
}
}
“扩展”方法比直接将代码添加到类中更脆弱,因为现在的同步策略实现被分不到多个单独维护的源代码中。如果底层的类改变了同步策略并选择了不同的锁类保护它的状态变量,那么子类会被破坏,因为在同步策略改变后它无法再使用正确的锁类控制对基类状态的并发访问。
4.4.1 客户端加锁机制
对于由 Collections.synchronizedList
封装的 ArrayList
,这两种方法在原始类中添加一个方法或者对类进行扩展都行不通,因为客户代码并不知道在同步封装器工厂方法中返回的 List
对象的类型。第三种策略是扩展类的功能,但并不扩展类本身,而是将扩展代码放入一个“辅助类”中。
// 程序清单 4-14
@NotThreadSafe
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<>());
// 这里的 synchronized 关键字不起作用:在错误的锁上进行了同步。
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
上面程序清单 4-15 中,synchronized
关键字持有的是 ListHelper
的内置锁,而不是 list
相关操作的锁,这就意味着 putIfAbsent
相对于 List
的其他操作来说并不是原子的,因此 ListHelper
并不是线程安全的。
要想使这个方法能正确执行,必须使 List
在实现客户端加锁或外部加锁时使用同一个锁。
// 程序清单 4-15
@NotThreadSafe
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<>());
public boolean putIfAbsent(E x) {
// 使用 list 自己的锁
synchronized (list) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
}
客户端加锁机制与扩展类机制有许多共同点,二者都是将派生类的行为与基类的实现耦合在一起。
4.4.2 组合
当为现有的类添加一个原子操作时,有一种更好的方法:组合(Composition)。
// 程序清单 4-16
@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(T x) {
boolean contains = list.contains(x);
if (contains)
list.add(x);
return !contains;
}
public synchronized void clear() {
list.clear();
}
// 按照类似的方式委托 List 的其他方法
}
ImprovedList
通过自身的内置锁增加了一层额外的加锁。它并不关心底层的 List
是否是线程安全的,即使 List
不是线程安全的或者修改了它的加锁实现,ImprovedList
也会提供一致的加锁机制来实现线程安全性。
而且,额外的同步层导致的性能损失其实非常小,因为在底层 List
上的同步不存在竞争,所以速度很快。