设计线程安全的类
在设计线程安全类的过程中 需要包含以下三个基本要素:
使用java监视器模式的线程安全计数器
@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;
}
}
收集同步需求
如果不了解对象的不变性条件与后验条件 那么就不能确保线程安全性 要满足在状态变量的有效值或状态转换上的各种约束条件 就需要借助于原子性与封装性
依赖状态的操作
如果在某个操作中包含有基于状态的先验条件 那么这个操作就称为依赖状态的操作 例如 不能从空队列中移除一个元素 在删除元素前 队列必须处于 非空的 状态
状态的所有权
对象封装它拥有的状态 反之也成立 即对它封装的状态拥有所有权 状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性 所有权意味着控制权
实例封闭
将数据封装在对象内部 可以将数据的访问限制在对象的方法上 从而更容易确保线程在访问数据时总能持有正确的锁
通过封闭机制来确保线程安全
@ThreadSafe
public class PersonSet {
@GuardedBy("this") private final Set mySet = new HashSet();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
interface Person {
}
}
封闭机制更易于构造线程安全的类 因为当封闭类的状态时 在分析类的线程安全性时就无须检查整个程序
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
}
}
}
使用私有的锁对象而不是对象的内置锁(或任何其他可通过公有方式访问的锁) 有许多优点 私有的锁对象可以将锁封装起来 使客户代码无法得到锁 但客户代码可以通过公有方法来访问锁 以便(正确或者不正确地)参与到它的同步策略中 如果客户代码错误地获得了另一个对象的锁 那么可能会产生活跃性问题 此外 要想验证某个公有访问的锁在程序中是否被正确地使用 则需要检查整个程序 而不是单个的类
示例:车辆追踪
基于监视器模式的车辆追踪
@ThreadSafe
public class MonitorVehicleTracker {
@GuardedBy("this") private final Map locations;
public MonitorVehicleTracker(Map locations) {
this.locations = deepCopy(locations);
}
public synchronized Map 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 deepCopy(Map m) {
Map result = new HashMap();
for (String id : m.keySet())
result.put(id, new MutablePoint(m.get(id)));
return Collections.unmodifiableMap(result);
}
}
与Java.awt.Point类似的可变Point类(不要这么做)
@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就要复制数据 因此将出现一种错误情况 虽然车辆的实际位置发生了变化 但返回的信息却保持不变
线程安全性的委托
在前面的CountingFactorizer类中 我们在一个无状态的类中增加了一个AtomicLong类型的域 并且得到的组合对象仍然是线程安全的 由于CountingFactorizer的状态就是AtomicLong的状态 而AtomicLong是线程安全的 因此CountingFactorizer不会对counter的状态施加额外的有效性约束 所以很容易知道CountingFactorizer是线程安全的 我们可以说CountingFactorizer将它的线程安全性委托给AtomicLong来保证 之所以CountingFactorizer是线程安全的 是因为AtomicLong是线程安全的
如果count不是final类型 那么要分析CountingFactorizer的线程安全性将变得更复杂 如果CountingFactorizer将count修改为指向另一个AtomicLong域的引用 那么必须确保count的更新操作对于所有访问count的线程都是可见的 并且还要确保在count的值上不存在竞态条件 这也是尽可能使用final类型域的另一个原因
示例:基于委托的车辆追踪器
在DelegatingVehicleTracker中使用的不可变Point类
@Immutable
public class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
将线程安全委托给ConcurrentHashMap
@ThreadSafe
public class DelegatingVehicleTracker {
private final ConcurrentMap locations;
private final Map unmodifiableMap;
public DelegatingVehicleTracker(Map points) {
locations = new ConcurrentHashMap(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map 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 getLocationsAsStatic() {
return Collections.unmodifiableMap(
new HashMap(locations));
}
}
如果使用最初的MutablePoint类而不是Point类 就会破坏封装性 因为getLocations会发布一个指向可变状态的引用 而这个引用不是线程安全的 在使用监视器模式的车辆追踪器中返回的是车辆位置的快照 而在使用委托的车辆追踪器中返回的是一个不可修改但却实时的车辆位置视图 这意味着 如果线程A调用getLocations 而线程B在随后修改了某些点的位置 那么在返回给线程A的Map中将反映出这些变化 这可能是一种优点(更新的数据) 也可能是一种缺点(可能导致不一致的车辆位置视图) 具体情况取决于你的需求
如果需要一个不发生变化的车辆视图 那么getLocations可以返回对locations这个Map对象的一个浅拷贝(Shallow Copy) 由于Map的内容是不可变的 因此只需复制Map的结构 而不用复制它的内容
返回locations的静态拷贝而非实时拷贝 其中只返回一个HashMap 因为getLocations并不能保证返回一个线程安全的Map
@ThreadSafe
public class DelegatingVehicleTracker {
private final ConcurrentMap locations;
private final Map unmodifiableMap;
public DelegatingVehicleTracker(Map points) {
locations = new ConcurrentHashMap(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map 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 getLocationsAsStatic() {
return Collections.unmodifiableMap(
new HashMap(locations));
}
}
独立的状态变量
到目前为止 这些委托示例都仅仅委托给了单个线程安全的状态变量 我们还可以将线程安全性委托给多个状态变量 只要这些变量是彼此独立的 即组合而成的类并不会在其包含的多个状态变量上增加任何不变性条件
将线程安全性委托给多个状态变量
public class VisualComponent {
private final List keyListeners
= new CopyOnWriteArrayList();
private final List mouseListeners
= new CopyOnWriteArrayList();
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这样简单:在它们的状态变量之间存在着某些不变性条件
NumberRange类并不足以保护它的不变性条件(不要这么做)
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都是 先检查后执行 的操作 但它们没有使用足够的加锁机制来保证这些操作的原子性 由于状态变量lower和upper不是彼此独立的 因此NumberRange不能将线程安全性委托给它的线程安全状态变量
NumberRange可以通过加锁机制来维护不变性条件以确保其线程安全性 例如使用一个锁来保护lower和upper 此外 它还必须避免发布lower和upper 从而防止客户代码破坏其不变性条件
如果某个类含有复合操作 例如NumberRange 那么仅靠委托并不足以实现线程安全性 在这种情况下 这个类必须提供自己的加锁机制以保证这些复合操作都是原子操作 除非整个复合操作都可以委托给状态变量
即使NumberRange 的各个状态组成部分都是线程安全的 也不能确保NumberRange 的线程安全性 这种问题非常类似于volatile变量规则:仅当一个变量参与到包含其他状态变量的不变性条件时 才可以声明为volatile类型
如果一个类是由多个独立且线程安全的状态变量组成 并且在所有的操作中都不包含无效状态转换 那么可以将线程安全性委托给底层的状态变量
发布底层的状态变量
如果一个状态变量是线程安全的 并且没有任何不变性条件来约束它的值 在变量的操作上也不存在任何不允许的状态转换 那么就可以安全地发布这个变量
示例:发布状态的车辆追踪器
线程安全且可变的Point类
@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 locations;
private final Map unmodifiableMap;
public PublishingVehicleTracker(Map locations) {
this.locations = new ConcurrentHashMap(locations);
this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
}
public Map 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中的元素是线程安全的且可变的Point 而并非不可变的 getLocation方法返回底层Map对象的一个不可变副本 调用者不能增加或删除车辆 但却可以通过修改返回Map中的SafePoint值来改变车辆的位置 Map的这种 实时 特性究竟是带来好处还是坏处 仍然取决于实际的需求 PublishingVehicleTracker是线程安全的 但如果它在车辆位置的有效值上施加了任何约束 那么就不再是线程安全的 如果需要对车辆位置的变化进行判断或者当位置变化时执行一些操作 那么PublishingVehicleTracker中采用的方法并不合适
在现有的线程安全类中添加功能
扩展Vector并增加一个 若没有则添加 方法
@ThreadSafe
public class BetterVector extends Vector {
// When extending a serializable class, you should redefine serialVersionUID
static final long serialVersionUID = -3963416950630760754L;
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if (absent)
add(x);
return absent;
}
}
扩展 方法比直接将代码添加到类中更加脆弱 因为现在的同步策略实现被分布到多个单独维护的源代码文件中 如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量 那么子类会被破坏 因为在同步策略改变后它无法再使用正确的锁来控制对基类状态的并发访问(在Vector的规范中定义了它的同步策略 因此BetterVector不存在这个问题)
客户端加锁机制
非线程安全的 若没有则添加(不要这么做)
@NotThreadSafe
class BadListHelper {
public List list = Collections.synchronizedList(new ArrayList());
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
ListHelper只是带来了同步的假象 尽管所有的链表操作都被声明为synchronized 但却使用了不同的锁 这意味着putIfAbsent相对于List的其他操作来说并不是原子的 因此就无法确保当putIfAbsent执行时另一个线程不会修改链表
要想使这个方法能正确执行 必须使List在实现客户端加锁或外部加锁时使用同一个锁 客户端加锁是指 对于使用某个对象X的客户端代码 使用X本身用于保护其状态的锁来保护这段客户代码 要使用客户端加锁 你必须知道对象X使用的是哪一个锁
在Vector和同步封装器类的文档中指出 它们通过使用Vector或封装器容器的内置锁来支持客户端加锁
通过客户端加锁来实现 若没有则添加
@ThreadSafe
class GoodListHelper {
public List list = Collections.synchronizedList(new ArrayList());
public boolean putIfAbsent(E x) {
synchronized (list) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
}
通过添加一个原子操作来扩展类是脆弱的 因为它将类的加锁代码分布到多个类中 然而 客户端加锁却更加脆弱 因为它将类C的加锁代码放到与C完全无关的其他类中 当在那些并不承诺遵循加锁策略的类上使用客户端加锁时 要特别小心
客户端加锁机制与扩展类机制有许多共同点 二者都是将派生类的行为与基类的实现耦合在一起 正如扩展会破坏实现的封装性 客户端加锁同样会破坏同步策略的封装性
组合
当为现有的类添加一个原子操作时 有一种更好的方法:组合(Composition)
通过组合实现 若没有则添加
@ThreadSafe
public class ImprovedList implements List {
private final List list;
/**
* PRE: list argument is thread-safe.
*/
public ImprovedList(List 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 iterator() {
return list.iterator();
}
public Object[] toArray() {
return list.toArray();
}
public 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 listIterator() {
return list.listIterator();
}
public ListIterator listIterator(int index) {
return list.listIterator(index);
}
public List subList(int fromIndex, int toIndex) {
return list.subList(fromIndex, toIndex);
}
public synchronized void clear() { list.clear(); }
}
ImprovedList通过自身的内置锁增加了一层额外的加锁 它并不关心底层的List是否是线程安全的 即使List不是线程安全的或者修改了它的加锁实现 ImprovedList也会提供一致的加锁机制来实现线程安全性 虽然额外的同步层可能导致轻微的性能损失 性能损失很小 因为在底层List上的同步不存在竞争 所以速度很快 但与模拟另一个对象的加锁策略相比 ImprovedList更为健壮 事实上 我们使用了Java监视器模式来封装现有的List 并且只要在类中拥有指向底层List的唯一外部引用 就能确保线程安全性
将同步策略文档化
在维护线程安全性时 文档是最强大的(同时也是最未被充分利用的)工具之一 用户可以通过查阅文档来判断某个类是否是线程安全的 而维护人员也可以通过查阅文档来理解其中的实现策略 避免在维护过程中破坏安全性 然而 通常人们从文档中获取的信息却是少之又少
在文档中说明客户代码需要了解的线程安全性保证 以及代码维护人员需要了解的同步策略
解释含糊的文档
许多Java技术规范都没有(或者至少不愿意)说明接口的线程安全性 例如ServletContext HttpSession或DataSource