目录
1、设计线程安全的类
在设计线程安全类的过程中,需要包含以下三个基本要素:
- 找出构成对象状态的所有变量。//所有成员变量
- 找出约束状态变量的不变性条件。
- 建立对象状态的并发访问管理策略。//对共享变量的访问进行同步处理
同步策略 (Synchronization Policy)定义了如何在不违背对象不变条件或后验条件的情况下对其状态的访问操作进行协同。同步策略规定了如何将不可变性、线程封闭与加锁机制等结合起来以维护线程的安全性,并且还规定了哪些变量由哪些锁来保护。//后验条件:操作后验证
1.1 - 收集同步需求
要确保类的线程安全性,就需要确保它的不变性条件不会在并发访问的情况下被破坏。对象与变量都有一个状态空间,即所有可能的取值。状态空间越小,就越容易判断线程的状态。final 类型的域使用得越多,就越能简化对象可能状态的分析过程。//在并发时状态稳定,避免竞态条件
同样,在操作中还会包含一些后验条件来判断状态迁移是否是有效的。如果计算器 counter 的当前状态为 17,那么下一个有效状态只能是 18。当下一个状态需要依赖当前状态时,这个操作就必须是一个复合操作。//并发时,复合操作可能会出现失效值及竞态条件 -> 原子操作
由于不变性条件以及后验条件在状态及状态转换上施加了各种约束,因此就需要额外的同步与封装。如果在某个操作中存在无效的状态转换,那么该操作必须是原子的。另外,如果在类中没有施加这种约束,那么就可以放宽封装性或序列化等需求,以便获得更高的灵活性或性能。
如果不了解对象的不变性条件与后验条件,那么就不能确保线程安全性。要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助于原子性与封装性。
1.2 - 依赖状态的操作:先验条件
类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换是有效的。在某些对象的方法中还包含一些基于状态的先验条件(Precondition)。例如,不能从空队列中移除一个元素,在删除元素前,队列必须处于“非空的”状态。如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作。//先验条件 -> 条件为真,则可操作
1.3 - 状态的所有权
许多情况下,所有权与封装性总是相互关联的:对象封装它拥有的状态,即对它封装的状态拥有所有权。
状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性。所有权意味着控制权。然而,如果发布了某个可变对象的引用,那么就不再拥有独占的控制权,最多是“共享控制权”。对于从构造函数或者从方法中传递进来的对象,类通常并不拥有这些对象,除非这些方法是被专门设计为转移传递进来的对象的所有权(例如,同步容器封装器的工厂方法)。
与所有共享对象一样,它们必须安全地被共享。为了防止多个线程在并发访问同一个对象时产生的相互干扰,这些对象应该要么是线程安全的对象,要么是事实不可变的对象,或者由锁来保护的对象。//当一个对象不具有独占控制权时,它就是共享对象
2、对实例进行封闭
如果某对象不是线程安全的,那么可以通过多种技术使其在多线程程序中安全地使用。你可以确保该对象只能由单个线程访问(线程封闭),或者通过一个锁来保护对该对象的所有访问。
封装简化了线程安全类的实现过程,它提供了一种实例封闭机制 (Instance Confinement),通常也简称为“封闭”。当一个对象被封装到另一个对象中时,能够访问被封装对象的所有代码路径都是已知的。与对象可以由整个程序访问的情况相比,更易于对代码进行分析。通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
//线程安全的类
public class PersonSet {
//非线程安全对象
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);
}
}
上边代码 PersonSet 说明了如何通过封闭与加锁等机制使一个类成为线程安全的类(即使这个类的状态变量并不是线程安全的)。PersonSet 的状态由 HashSet 来管理的,而 HashSet 并非线程安全的。但由于 mySet 是私有的并且不会逸出,因此 HashSet 被封闭在 PersonSet 中。唯一能访问 mySet 的代码路径是 addPerson 与 containsPerson,在执行它们时都要获得 PersonSet 上的锁。PersonSet 的状态完全由它的内置锁保护,因而 PersonSet 是一个线程安全的类。//在这个示例中,也可以使用同步容器/并发容器来包装 Person
封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就无须检查整个程序。
2.1 - Java 监听器模式
遵循 Java 监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。使用 Java 监视器模式的代码如下所示:
//通过一个私有锁来保护状态
public class PrivateLock {
//私有锁
private final Object myLock = new Object();
private Widget widget;
void someMethod() {
synchronized (myLock) {
// Access or modify the state of widget
}
}
}
使用私有的锁对象而不是对象的内置锁(或任何其他可通过公有方式访问的锁),有许多优点。私有的锁对象可以将锁封装起来,使客户代码无法得到锁,但客户代码也可以通过公有方法来访问锁,以便(正确或者不正确地)参与到它的同步策略中。
2.2 - 基于监听器模式的车辆追踪:使用快照
//车辆追踪:线程安全的类
public class MonitorVehicleTracker {
/**
* 成员变量:不可变对象,不对外暴露
*/
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<>();
for (String id : m.keySet()) {
result.put(id, new MutablePoint(m.get(id)));
}
//返回不可修改的视图:一致性快照
return Collections.unmodifiableMap(result);
}
}
//非线程安全的类
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 值都相同。
在某种程度上,这种实现方式是通过在返回客户代码之前复制可变的数据来维持线程安全性的。通常情况下,这并不存在性能问题,但在车辆容器非常大的情况下将极大地降低性能,此外,由于每次调用 getLocation 就要复制数据,因此将出现一种错误情况一一虽然车辆的实际位置发生了变化,但返回的信息却保持不变。// 使用快照是否合适,取决于具体需求
3、线程安全的委托:使用并发容器
在代码 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<>(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);
}
}
// getLocations 的替代版本
public Map<String, Point> getLocationsAsStatic() {
return Collections.unmodifiableMap(new HashMap<>(locations));
}
}
//不可变对象:不可变的值可以被自由地共享与发布
public final class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
上边代码稍微改变了车辆追踪器类的行为,在使用监视器模式的车辆追踪器中返回的是车辆位置的快照,而在使用委托的车辆追踪器中返回的是一个不可修改但却实时的车辆位置视图。这意味着,如果线程 A 调用 getLocations,而线程 B 在随后修改了某些点的位置,那么在返回给线程A的 Map 中将反映出这些变化。//使用并发容器能获取数据的实时变化
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。
4、在现有的线程安全类中添加功能:组合
当为现有的类添加一个原子操作时,可以使用组合(Composition)方法。下列代码中,ImprovedList 通过将 List 对象的操作委托给底层的 List 实例来实现 List 的操作,同时还添加了一个原子的 putIfAbsent 方法。//对原有对象进行增强和包装
//@ThreadSafe
public class ImprovedList<T> implements List<T> {
private final List<T> list;
/**
* 入参list是线程安全的
*/
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;
}
// Plain vanilla delegation for List methods.
// Mutative methods must be synchronized to ensure atomicity of putIfAbsent.
public int size() {
return list.size();
}
public boolean isEmpty() {
return list.isEmpty();
}
public boolean contains(Object o) {
return list.contains(o);
}
public Iterator<T> iterator() {
return list.iterator();
}
public Object[] toArray() {
return list.toArray();
}
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}
public synchronized boolean add(T e) {
return list.add(e);
}
public synchronized boolean remove(Object o) {
return list.remove(o);
}
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
public synchronized boolean addAll(Collection<? extends T> c) {
return list.addAll(c);
}
public synchronized boolean addAll(int index, Collection<? extends T> c) {
return list.addAll(index, c);
}
public synchronized boolean removeAll(Collection<?> c) {
return list.removeAll(c);
}
public synchronized boolean retainAll(Collection<?> c) {
return list.retainAll(c);
}
public boolean equals(Object o) {
return list.equals(o);
}
public int hashCode() {
return list.hashCode();
}
public T get(int index) {
return list.get(index);
}
public T set(int index, T element) {
return list.set(index, element);
}
public void add(int index, T element) {
list.add(index, element);
}
public T remove(int index) {
return list.remove(index);
}
public int indexOf(Object o) {
return list.indexOf(o);
}
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
public ListIterator<T> listIterator() {
return list.listIterator();
}
public ListIterator<T> listIterator(int index) {
return list.listIterator(index);
}
public List<T> subList(int fromIndex, int toIndex) {
return list.subList(fromIndex, toIndex);
}
public synchronized void clear() {
list.clear();
}
}
ImprovedList 通过自身的内置锁增加了一层额外的加锁。它并不关心底层的 List 是否是线程安全的,即使 List 不是线程安全的或者修改了它的加锁实现,ImprovedList 也会提供一致的加锁机制来实现线程安全性。
虽然额外的同步层可能导致轻微的性能损失,但与模拟另一个对象的加锁策略相比,ImprovedList 更为健壮。事实上,我们使用了 Java 监视器模式来封装现有的 List,并且只要在类中拥有指向底层 List 的唯一外部引用,就能确保线程安全性。//在增强类中使用类的内置锁,而不是持有原 List 对象的锁,这样程序更为健壮:加锁时只会锁增强对象,而不是把 List 类锁了