Java对象的组合
万猫学社 人气:01. 设计线程安全的类
在设计线程安全类的过程中,需要包含以下三个基本要素:
- 找出构成对象状态的所有变量。
- 找出约束变量的不变性条件。
- 建立对象状态的并发访问管理策略。
1.1 收集同步需求
在很多类中都定义了一些不可变条件,用于判断状态是否有效。比如:计数器的取值范围上存在一个限制,就是不能是负值。在操作中还会包含一些后验条件来判断状态迁移是否有有效。比如:计数器的当前状态为8,那么下一个有效状态只能是9。由于不变性条件和后验条件在状态及状态转换上施加了各种约束,因此就需要额外的同步和封装。
1.2 依赖状态的操作
在某些对象的方法中还包含一些基于状态的先验条件。比如:不能从空队列中移除一个元素,在删除元素前,队列必须处于非空状态。如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作。
1.3 状态的所有权
对象封装了它拥有的状态,那么它就拥有封装状态的所有权。状态的所有者将决定采用何种加锁协议来维持状态的完整性。所有权意味着控制权。如果发布了某个可变对象的引用,那么就不再拥有独占的控制权,最多是共享控制权。为了防止多个线程在并发访问同一个对象时产生相互干扰,这些对象应该要么是线程安全的对象,要么是事实不可变的对象,或者用同一个锁保护的对象。
2. 实例封闭
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据是总能持有正确的锁。被封闭对象一定不能超过它们既定的作用域。对象可以封闭在类的一个实例中,比如:作为类的一个私有成员;或者封闭在某个作用域内,比如:作为一个局部变量;或者封闭在线程中,比如:在某个线程中将对象从一个方法传递到另一个方法,而不是在多个线程之间共享该对象。
封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就无须检测整个程序。
2.1 Java监视器模式
遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。示例:一个用于调度车辆的“车辆跟踪器”,每台车都由一个String对象来标识,并且拥有一个对应的位置坐标(x,y)。创建一个追踪器类用来封装所有车辆的标识和位置,该类由多个线程(读取操作和更新操作)共享。
@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; } } @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); } }
虽然类MutablePoint
不是线程安全的,但是追踪器类是线程安全的。它所包含的Map对象和可变的Point对象都未曾发布。当需要回去车辆的位置时,通过拷贝构造函数或deepCopy方法复制正确的值,从而生成一个新的Map对象。车辆少是,这并不存在性能问,单在车辆数量非常大的情况下将极大地降低性能。
3. 线程安全性的委托
大多数对象都是组合对象。当从头开始构建一个类,或者将多个非线程安全的类组合为一个类时,Java监视器模式是非常有用的。但是,如果类中的各个组件都已经是线程安全的,那么实现线程安全就应视情况而定。
3.1 基于委托的车辆追踪器
保存车辆位置信息的Map类,使用线程安全的ConcurrentMap
类代替。
使用不可变的Point类来代替MutablePoint类:
public class Point { public final int x, y; public Point(int x, int y) { this.x = x; this.y = y; } }
在DelegatingVehicleTracker中没有使用显式的同步,所有对状态的访问都由ConcurrentHashMap来管理:
@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); } }
在使用监视器模式的车辆追踪器中返回的是车辆位置的快照,而在使用委托的车辆追踪器中返回的是一个不可修改却实时的车辆位置信息。
3.2 独立的状态变量
我们还可以将线程安全性委托给多个状态变量,只要这些变量是彼此独立的,即组合而成的类并不会在其包含的多个状态变量上增加任何不变形条件。
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
类是一个图形组件,允许客户程序注册监控鼠标和键盘等事件的监听器。在鼠标事件监听器和键盘事件监听器之间不存在任何关联,二者是彼此独立的,因此VisualComponent
可以将其线程安全性委托给这两个线程安全的监听器列表。
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。
3.3 发布底层的状态变量
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。我们来修改之前的车辆追踪器的代码,使其发布底层的可变状态。
首先写一个可变并且线程安全的Point类:
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; } }
再修改追踪器的代码:
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); } }
在PublishingVehicleTracker
中,其线程安全性委托给底层的ConcurrentHashMap
,只是Map中的元素是线程安全的可变的。getLocations
方法返回底层Map对象的一个不可变副本,调用者不能增加或删除车辆,但可以通过修改返回Map中的SafePoint
值来修改车辆的问题。
加载全部内容