3.多线程之对象的组合
一.设计线程安全的类
分析对象的域,如果对象中所有的域都是基本类型的变量,那么这些域将构成对象的全部状态。
1.收集同步需求
一个类包含两个状态变量,分别表示范围的上界和下届。这些变量必须遵循的约束是,下界值应该小于或等于上界值。类似于这种包含多个变量的不变性条件将带来原子性需求:这些相关的变量必须在单个原子操作中进行读取或更新。不能首先更新一个变量,然后释放锁并再次获得锁,然后再更新其他的变量。因为释放锁后,可能会使对象处于无效状态。如果在一个不变性条件中包含多个变量,那么在执行任何访问相关变量的操作时,都必须持有保护这些变量的锁。
2.依赖状态的操作
类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换是有效的。在某些对象的方法中还包含一些基于状态的先验条件( Precondition)。例如,不能从空队列中移除一个元素,在删除元素前,队列必须处于“非空的”状态。如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作。
二.实例封闭
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保?线程在访问数据时总能持有正确的锁。
@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); } interface Person { } }
上面PersonSet说明了如何通过封闭与加锁等机制使一个类成为线程安全的(即使这个类的状态变量并不是线程安全的)。PersonSet的状态由HashSet来管理的,而HashSet并非线程安全的。但由于mySet是私有的并且不会逸出,因此HashSet被封闭在PersonSet 中。唯一能访问mySet的代码路径是addPerson与containsPerson,在执行它们时都要获得PersonSet 上的锁。PersonSet的状态完全由它的内置锁保护,因而PersonSet是一个线程安全的类。
1.Java监视器模式
从线程封闭原则及其逻辑推论可以得出Java监视器模式。遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。例如在下面Counter 中封装了一个私有状态变量value,对该变量的所有访问都需要通过Counter的方法来执行,并且这些方法都是同步的。
@ThreadSafe public final class Counter { @GuardedBy("this") private long value = 0; public synchronized long getValue() { return value; } public synchronized long increment() { if (value == Long.MAX_VALUE) throw new IllegalStateException("counter overflow"); return ++value; } }
Java监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态。比如下面的示例:
public class PrivateLock { private final Object myLock = new Object(); @GuardedBy("myLock") Widget widget; void someMethod() { synchronized (myLock) { // Access or modify the state of widget } } }
2.示例:车辆追踪
例如出租车、警车、货车等。首先使用监视器模式来构建车辆追踪器,然后再尝试放宽某些封装性需求同时又保持线程安全性。 每台车都由一个String对象来标识,并且拥有一个相应的位置坐标(x,y)。在 VehicleTracker类中封装了车辆的标识和位置,因而它非常适合作为基于MVC (Model-View-Controller,模型–视图–控制器)模式的GUI应用程序中的数据模型,并且该模型将由一个视图线程和多个执行更新操作的线程共享。视图线程会读取车辆的名字和位置,并将它们显示在界面上。
类似地,执行更新操作的线程通过从GPS 设备上获取的数据或者调度员从GUI界面上输入的数据来修改车辆的位置。
@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<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; } }
虽然类MutablePoint 不是线程安全的,但追踪器类是线程安全的。它所包含的Map对象和可变的Point对象都未曾发布。当需要返回车辆的位置时,通过 MutablePoint 拷贝构造函数或者deepCopy方法来复制正确的值,从而生成一个新的Map对象,并且该对象中的值与原有Map对象中的key值和value值都相同。
三.线程安全性的委托
在前面的CountingFactorizer类中,我们在一个无状态的类中增加了一个AtomicLong类型的域,并且得到的组合对象仍然是线程安全的。由于CountingFactorizer的状态就是AtomicLong的状态,而AtomicLong是线程安全的,因此CountingFactorizer不会对counter的状态施加额外的有效性约束,所以很容易知道CountingFactorizer是线程安全的。我们可以说CountingFactorizer将它的线程安全性委托给AtomicLong 来保证:之所以CountingFactorizer是线程安全的,是因为AtomicLong是线程安全的。
1.示例:基于委托的车辆追踪器
下面将介绍一个更实际的委托示例,构造一个委托给线程安全类的车辆追踪器。我们将车辆的位置保存到一个Map对象中,因此首先要实现一个线程安全的Map类,ConcurrentHashMap。我们还可以用一个不可变的Point类来代替MutablePoint以保存位置,如下面程序所示。
@Immutable public class Point { public final int x, y; public Point(int x, int y) { this.x = x; this.y = y; } }
由于Point类是不可变的,因而它是线程安全的。不可变的值可以被自由地共享与发布,因此在返回location时不需要复制。 在下面程序 DelegatingVehicleTracker中没有使用任何显式的同步,所有对状态的访问都由ConcurrentHashMap来管理,而且Map所有的键和值都是不可变的。
@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<String, Point>(points); unmodifiableMap = Collections.unmodifiableMap(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("invalid vehicle name: " + id); } // Alternate version of getLocations (Listing 4.8) public Map<String, Point> getLocationsAsStatic() { return Collections.unmodifiableMap( new HashMap<String, Point>(locations)); } }
3.当委托失效时
下面的NumberRange使用了两个 AtomicInteger来管理状态,并且含有一个约束条件,即第一个数值要小于或等于第二个数值。
public class NumberRange { // INVARIANT: lower <= upper private final AtomicInteger lower = new AtomicInteger(0); private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i) { // Warning -- unsafe check-then-act if (i > upper.get()) throw new IllegalArgumentException("can't set lower to " + i + " > upper"); lower.set(i); } public void setUpper(int i) { // Warning -- unsafe check-then-act 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不能将线程安全性委托给它的线程安全状态变量。
4.发布底层的状态变量
如果一个状态变量是线程安全的,并且没有任何不变性条件来约来它的值,在变量的操作上也不存在任何不充许的状态转换,那么就可以安全地发布这个变量。
5.示例:发布状态的车辆追踪器
下面SafePoint提供的get方法同时获得x和y的值,并将二者放在一个数组中返回。如果为x和y分别提供get方法,那么在获得这两个不同坐标的操作之间,x和y的值发生变化,从而导致调用者看到不一致的值﹔车辆从来没有到达过位置(x,y)。通过使用SafePoint,可以构造一个发布其底层可变状态的车辆追踪器,还能确保其线程安全性不被破坏,如下面的Publishing VehicleTracker类所示。
@ThreadSafe 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.set(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; } }
@ThreadSafe 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); } }
四.在现有的线程安全类中添加功能
比如需要一个这样的需求:如果一个线程安全的list中没有某个X元素,则添加。由于这个操作不是原子的,所以并不是线程安全的,可能两个线程同时检测到X不存在,然后同时将X添加进了List中。
1.客户端加锁机制
@ThreadSafe class GoodListHelper <E> { public List<E> list = Collections.synchronizedList(new ArrayList<E>()); public boolean putIfAbsent(E x) { synchronized (list) { boolean absent = !list.contains(x); if (absent) list.add(x); return absent; } } }
2.组合
@ThreadSafe public class ImprovedList<T> implements List<T> { private final List<T> list; /** * PRE: list argument is thread-safe. */ 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; } }
ImprovedList通过自身的内置锁增加了一层额外的加锁。它并不关心底层的List是否是线程安全的,即使List 不是线程安全的或者修改了它的加锁实现,ImprovedList 也会提供一致的加锁机制来实现线程安全性。虽然额外的同步层可能导致轻微的性能损失,但与模拟另一个对象的加锁策略相比,ImprovedList更为健壮。事实上,我们使用了Java监视器模式来封装现有的List,并且只要在类中拥有指向底层List的唯一外部引用,就能确保线程安全性。