4.3 线程安全性的委托
大多数对象都是组合对象。当从头开始构建一个类,或者将多个非线程安全的类组合为一个类时, Java 监视器模式是非常有用的。但是,如果类中的各个组件都已经是线程安全的,会是什么情况呢?我们是否需要再增加一个额外的线程安全层?答案是“视情况而定”。在某些情况下,通过多个线程安全类组合而成的类是线程安全的(如程序清单4-7 和程序清单4-9所示),而在某些情况下,这仅仅是一个好的开端(如程序清单4-10所示)。
⊖注意,deepCopy并不只是用unmodifiableMap来包装Map的,因为这只能防止容器对象被修改,而不能防止调用者修改保存在容器中的可变对象。基于同样的原因,如果只是通过拷贝构造函数来填充deepCopy中的HashMap,那么同样是不正确的,因为这样做只复制了指向Point对象的引用,而不是Point对象本身。
② 由于deepCopy是从一个synchronized 方法中调用的,因此在执行时间较长的复制操作中, tracker的内置锁将一直被占有,当有大量车辆需要追踪时,会严重降低用户界面的响应灵敏度。
在前面的CountingFactorizer类中,我们在一个无状态的类中增加了一个AtomicLong 类型的域,并且得到的组合对象仍然是线程安全的。由于CountingFactorizer的状态就是AtomicLong的状态,而AtomicLong 是线程安全的,因此CountingFactorizer不会对counter的状态施加额外的有效性约束,所以很容易知道CountingFactorizer 是线程安全的。我们可以说CountingFactorizer将它的线程安全性委托给AtomicLong来保证:之所以CountingFactorizer是线程安全的,是因为AtomicLong是线程安全的。
4.3.1 示例:基于委托的车辆追踪器
下面将介绍一个更实际的委托示例,构造一个委托给线程安全类的车辆追踪器。我们将车辆的位置保存到一个Map对象中,因此首先要实现一个线程安全的Map类,ConcurrentHashMap。我们还可以用一个不可变的Point类来代替MutablePoint以保存位置,如程序清单4-6所示。
public class Point {.
public final int x,y;
public Point(int x, int y){
this. x = x;
this. y = y;
}
}
由于Point类是不可变的,因而它是线程安全的。不可变的值可以被自由地共享与发布,因此在返回location时不需要复制。
在程序清单4-7的DelegatingVehicleTracker中没有使用任何显式的同步,所有对状态的访问都由ConcurrentHashMap来管理,而且Map 所有的键和值都是不可变的。
public class DelegatingVehicleTracker {
private final ConcurrentMap<String, Point>locations;
private final Map<String, Point>unmodifiableMap;
public DelegatingVehicleTracker(Map<String, Point>points){
locations =new ConcurrentHashMap<String, Point>(points);
unmodifiableMap =Collections. unmodifiableMap(locations);
}
- 如果count不是final 类型,那么要分析CountingFactorizer的线程安全性将变得更复杂。如果CountingFactorizer 将count修改为指向另一个AtomicLong域的引用,那么必须确保count的更新操作对于所有访问count 的线程都是可见的,并且还要确保在count 的值上不存在竞态条件。这也是尽可能使用final 类型域的另一个原因。
public Map<String, Point>getLocations(){
r eturn 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(
如果使用最初的MutablePoint类而不是Point类,就会破坏封装性,因为getLocations会发布一个指向可变状态的引用,而这个引用不是线程安全的。需要注意的是,我们稍微改变了车辆追踪器类的行为。在使用监视器模式的车辆追踪器中返回的是车辆位置的快照,而在使用委托的车辆追踪器中返回的是一个不可修改但却实时的车辆位置视图。这意味着,如果线程A 调用getLocations,而线程B在随后修改了某些点的位置,那么在返回给线程A的Map 中将反映出这些变化。在前面提到过,这可能是一种优点(更新的数据),也可能是一种缺点(可能导致不一致的车辆位置视图),具体情况取决于你的需求。
如果需要一个不发生变化的车辆视图,那么getLocations 可以返回对locations 这个Map对象的一个浅拷贝(Shallow Copy)。由于Map 的内容是不可变的,因此只需复制Map 的结构,而不用复制它的内容,如程序清单4-8 所示(其中只返回一个HashMap,因为getLocations并不能保证返回一个线程安全的Map)。
public Map<String, Point>getLocations(){
return Collections,unmodifiableMap(
new HashMap<String, Point>(locations));
}
4.3.2 独立的状态变量
到目前为止,这些委托示例都仅仅委托给了单个线程安全的状态变量。我们还可以将线程安全性委托给多个状态变量,只要这些变量是彼此独立的,即组合而成的类并不会在其包含的多个状态变量上增加任何不变性条件。
程序清单4-9 中的VisualComponent是一个图形组件,允许客户程序注册监控鼠标和键盘等事件的监听器。它为每种类型的事件都备有一个已注册监听器列表,因此当某个事件发生时,就会调用相应的监听器。然而,在鼠标事件监听器与键盘事件监听器之间不存在任何关联,二者是彼此独立的,因此VisualComponent可以将其线程安全性委托给这两个线程安全的监听器列表。
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);
}
}
VisualComponent 使用CopyOnWriteArrayList来保存各个监听器列表。它是一个线程安全的链表,特别适用于管理监听器列表(参见5.2.3节)。每个链表都是线程安全的,此外,由于各个状态之间不存在耦合关系,因此VisualComponent可以将它的线程安全性委托给mouseListeners和keyListeners等对象。
4.3.3 当委托失效时
大多数组合对象都不会像VisualComponent 这样简单:在它们的状态变量之间存在着某些不变性条件。程序清单4-10中的NumberRange 使用了两个AtomicInteger 来管理状态,并且含有一个约束条件,即第一个数值要小于或等于第二个数值。
// 注意——不安全的 “先检查后执行”public void setLower(int i){
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 ());
}
}
NumberRange 不是线程安全的,没有维持对下界和上界进行约束的不变性条件。setLower 和setUpper等方法都尝试维持不变性条件,但却无法做到。setLower和setUpper都是“先检查后执行”的操作,但它们没有使用足够的加锁机制来保证这些操作的原子性。假设取值范围为(0,10),如果一个线程调用setLower(5),而另一个线程调用setUpper(4),那么在一些错误的执行时序中,这两个调用都将通过检查,并且都能设置成功。结果得到的取值范围就是(5,4),那么这是一个无效的状态。因此,虽然AtomicInteger是线程安全的,但经过组合得到的类却不是。由于状态变量lower 和upper 不是彼此独立的,因此NumberRange不能将线程安全性委托给它的线程安全状态变量。
NumberRange 可以通过加锁机制来维护不变性条件以确保其线程安全性,例如使用一个锁来保护lower 和upper。此外,它还必须避免发布lower 和upper,从而防止客户代码破坏其不变性条件。
如果某个类含有复合操作,例如NumberRange,那么仅靠委托并不足以实现线程安全性。在这种情况下,这个类必须提供自己的加锁机制以保证这些复合操作都是原子操作,除非整个复合操作都可以委托给状态变量。
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。
即使NumberRange的各个状态组成部分都是线程安全的,也不能确保NumberRange的线程安全性,这种问题非常类似于3.1.4节介绍的volatile 变量规则:仅当一个变量参与到包含其他状态变量的不变性条件时,才可以声明为volatile类型。
4.3.4 发布底层的状态变量
当把线程安全性委托给某个对象的底层状态变量时,在什么条件下才可以发布这些变量从而使其他类能修改它们?答案仍然取决于在类中对这些变量施加了哪些不变性条件。虽然Counter 中的value域可以为任意整数值,但Counter施加的约束条件是只能取正整数,此外递增操作同样约束了下一个状态的有效取值范围。如果将value声明为一个公有域,那么客户代码可以将它修改为一个无效值,因此发布value 会导致这个类出错。另一方面,如果某个变量
表示的是当前温度或者最近登录用户的ID,那么即使另一个类在某个时刻修改了这个值,也不会破坏任何不变性条件,因此发布这个变量也是可以接受的。(这或许不是个好主意,因为发布可变的变量将对下一步的开发和派生子类带来限制,但不会破坏类的线程安全性。)
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。
例如,发布VisualComponent中的mouseListeners或keyListeners等变量就是安全的。由于VisualComponent 并没有在其监听器链表的合法状态上施加任何约束,因此这些域可以声明为公有域或者发布,而不会破坏线程安全性。
4.3.5 示例:发布状态的车辆追踪器
我们来构造车辆追踪器的另一个版本,并在这个版本中发布底层的可变状态。我们需要修改接口以适应这种变化,即使用可变且线程安全的Point类。
程序清单4-11 中的SafePoint提供的get 方法同时获得x和y的值,并将二者放在一个数组中返回。如果为x和y分别提供get方法,那么在获得这两个不同坐标的操作之间,x和y 的值发生变化,从而导致调用者看到不一致的值:车辆从来没有到达过位置(x,y)。通过使用SafePoint,可以构造一个发布其底层可变状态的车辆追踪器,还能确保其线程安全性不被破坏,如程序清单4-12 中的PublishingVehicleTracker类所示。
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 SafePoint(int x, int y){
this . x = x;
this. y = y;
}
public synchronized int []get(){
return new int[] { x,y };
}
public synchronized void set(int x, int y){
this. x = x;
this. y = Y;
}
}
如果将拷贝构造函数实现为this (p. x,p. y),那么会产生竞态条件,而私有构造函数则可以避免这种竞态条件。这是私有构造函数捕获模式(Private Constructor Capture Idiom, Bloch and Gafter,2005)的一个实例。
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);
}
}
PublichingVehicleTracker将其线程安全性委托给底层的ConcurrentHashMap,只是Map中的元素是线程安全的且可变的Point,而并非不可变的。getLocation方法返回底层Map 对象的一个不可变副本。调用者不能增加或删除车辆,但却可以通过修改返回Map 中的SafePoint值来改变车辆的位置。再次指出, Map的这种“实时”特性究竟是带来好处还是坏处,仍然取决于实际的需求。PublishingVehicleTracker是线程安全的,但如果它在车辆位置的有效值上施加了任何约束,那么就不再是线程安全的。如果需要对车辆位置的变化进行判断或者当位置变化时执行一些操作,那么PublishingVehicleTracker中采用的方法并不合适。