设计线程安全的类
设计安全的类的过程,需要包含三个要素:
- 找出构成对象状态的所有变量
- 找出状态变量的约束
- 建立对象状态的并发访问策略
这三条也是本篇文章的核心,以下将会逐一展开对象状态和约束都是什么,以及重点,如何结合三条要素,去设计并发访问策略。有三种常用的模式,封闭实例,委托,拓展。
状态和约束
状态就是对象中的状态变量,约束则是涉及到并发不可变性的种种约束。之所以放在一起讲,是因为本身约束就是作用于变量上。
找到状态和约束的过程,实际上也是收集同步需求的过程。
状态
对象的所有域都可能是状态,只要它们不是不可变的,如果域为引用类型,则引用对象的域也需要进行分析。
所以每个对象都会有一个状态变化空间,空间越小,这种分析越容易。从实践的角度来看,final的类型越多,不可变状态越多,这种分析越容易。
约束
状态之上的约束有三种:
- 前置:先验条件。例如,判断集合为空才可以操作。
- 后置:后验条件。例如,如果当前状态为14那么下一个状态必定是15。
- 不变性条件。例如,NumberRange中low < high。
这部分,我认为简单理解为状态上的约束即可。不管是多个之间的不可变性条件,或者是集合不可变等等,通通都属于状态上的约束。
封闭实例机制
封闭实例机制,就不得不说到Java监视器模式。Java监视器模式是实现封闭实例机制最常见的方式。
所谓的封闭实例机制,就是把对象所有可变状态都封装起来,并由对象自己的内置锁来保护。
它非常好理解。简单来说就是封装状态,提供受限的同步接口进行访问。例如以下代码,只要Person是线程安全的,那么mySet就是线程安全的。
@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);
}
}
我们尝试用开篇提到的三个要素来分析以下:
- 状态和约束
- mySet,添加需要线程安全
- Person,线程安全类(假设)
- 同步策略
- 使用Java监视器模式,对mySet进行封装,提供同步的添加和访问接口
- 用this锁保护状态的访问修改。
再来看一个更加复杂的例子,这个例子将会贯穿后文。
我们需要观察一个车辆位置的集合,跟踪车辆位置,例如绘图线程专门绘制这些点。我们需要获得某个点的位置,设置某个点的位置,获得所有车辆位置的视图。
这样的需求需要线程安全性,一种基于Java监视器的实现如下。
虽然MutablePoint不是线程安全的,但是MonitorVehicleTracker是线程安全的,它包含的map和point都没有发布(使用了保护性拷贝),并不会出现一致性问题(例如if 某个状态
,进入该分支后被中断,切回来发现状态变动,导致一致性问题)。
@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