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
是否是线程安全的,它都会通过一致的加锁机制来实现线程安全性。组合方式的加锁实现会比之前两种更为健壮。