第四章 组成(线程安全的)对象
4.1 设计线程安全的类
设计一个线程安全的类包含如下准则:
1、识别出哪些变量将改变类的状态
2、识别出约束状态变量的不变条件
3、建立起规则,用于管理并发访问状态的状态
如果一个对象的field都是由基本数据类型(int long等)组成的,则所有这些field就构成了对象的全部状态。
如果对象的field中还包含引用,则对象状态还要包括这些引用变量中的隐含数据。例如LinkedList中的Node。
为了方便后人阅读代码,应该在代码处对同步机制作出注释,特别是哪个类由哪个锁所保护,如下所示:
@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; } }
用标注的形式来表示这样:@GuardedBy("this")
4.1.1 收集同步需求
为了更好的了解同步需求,首先要根据field字段获得对象的状态空间。
这里面还要根据实际业务来确定,例如:
1、Long的范围是Long.MIN_VALUE到Long.MAX_VALUE。但对于计数器Counter而言,显然负数不是状态空间。
2、有的Field有后置条件:如果现在计数值是16,则下一个必须是17,不是所有的变量都有这种特性。
3、有些field是相互绑定的,比如对于Cache数、因子的例子,要么两个同时更新,要么不更新。更新一个忘了另一个是不符合对象的状态空间的。
4.1.2 状态相关的操作
除了静态层面上,还有一些对象由状态相关(State)的约束条件。
例如:从队列移除对象之前,队列必须是非空的。这类限定叫做状态相关的约束条件。
单线程条件下,不满足状态相关约束条件只能失败,但是并发程序中则不然。
线程等待的过程中,可能由于其他线程的作用,使约束条件变为有效。
关于等待条件为真的问题上,可由wait()和notify()加上内置锁来解决,但机制比较复杂难于实现。所以也可以用现成的类:例如BlockingQueue,Semphore。这些机制将在第5章进行介绍。
4.1.3 状态的所有权
Field的所有权不仅包含直接的field,还包含例如Map中的Map.Entry。对于这些Entry而言,其所有权是共享的。
举个例子:
ServletContext封装了类似Map的操作:setAttribute和getAttribute,ServletContext保证了其本身是线程安全的。也就是说get和set在调用时不需要额外的同步代码。但是对于get得到的对象的操作,必须进行同步控制,因为很有可能还有其他线程在对这些对象进行操作。要么保证对象是线程安全的,或者保证是不变的,或者加锁控制。
4.2 实例限制
尽管组成类的Field可能不是线程安全的,但是通过“实例限制”的方法,可以让它组成的类编程线程安全的。
如下所示:
Person并不是线程安全的,但是mySet是private的,不会外泄且只能通过addPerson和containsPerson访问,而这两个操作又是被锁定的。
由此将并非线程安全的Person组成了线程安全的PersonSet
@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); } }
尽管PersonSet是线程安全的了,但是如果Person是易变的,则最好也将Person变成线程安全的。
在JDK中,也使用了类似方法,将并非线程安全的ArrayList和HashMap变成了线程安全的Collections.synchronizedList等。通过包装的设计模式,将非线程的原始类限定在新类之内。包装方法(摘自JDK):
List list = Collections.synchronizedList(new ArrayList());
...
synchronized(list) {
Iterator i = list.iterator(); // Must be in synchronized block
while (i.hasNext())
foo(i.next());
}
注意尽管包装后的List是线程安全的了,但是迭代器遍历时候仍然要同步。
4.2.1 Java监视器模式
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 } } }
它自己创建了一个Object作为锁。Vector、Hashtable等都使用了类似的设计模式。使用这种私有锁而不是将对象作为锁(synchronized方法)的好处是:使得外界无法直接参与作用同步机制,防止了意外的发生。
来看一个例子:
@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; } }
可以注意到,MonitorVehicleTracker是一个线程安全的类。然而由于MutablePoint不是线程安全的,所以在从这个类返回的任何包含MutablePoint类都是经过拷贝的副本。这是常用的一个技巧,否则必须在程序所有地方对MutablePoint操作的时候加上同步控制。
4.3 授权线程安全
Java监视器模式可以很好地解决由非线程安全field构成的对象的并发访问问题。但对于field都是线程安全的情况,有时并不必这么做。
改用线程安全的点类(因为不可变所以线程安全):
@Immutable public class Point { public final int x, y; public Point(int x, int y) { this.x = x; this.y = y; } }
线程安全的这个Point可以随意被发布,也不用再return之前被拷贝。
这样之后,DelegatingVehichleTracker也不用再做同步控制了,因为ConcurrentMap保证了线程安全!!
@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); } }
4.4 向线程安全的类添加函数
有的时候,复用已有的类可以大大简化我们的工作量,尤其是线程安全的类。但他们往往缺少我们所需要的功能,因此可以拓展线程安全的类并添加自己需要的方法。
例如,我们需要List提供“没有则插入”的功能,一个很好的做法就是拓展已然是线程安全的Vector。
@ThreadSafe public class BetterVector<E> extends Vector<E> { public synchronized boolean putIfAbsent(E x) { boolean absent = !contains(x); if (absent) add(x); return absent; } }
直接使用vector提供的contains和add方法即可完成。但这两个操作不在一个原子操作里,因此必须用锁控制。否则可能出现插入两次的情况。
但是,并非所有时候都能很方便的进行extends,有时候我们需要构造一个Helper类来代理。
比如对于Collections.synchronizedList包装的ArrayList,客户端无法知道原来被包装的是ArrayList,更无从extends。
因此需要Helper。
下面是一个错误的例子:
@NotThreadSafe public class ListHelper<E> { public List<E> list = Collections.synchronizedList(new ArrayList<E>()); ... public synchronized boolean putIfAbsent(E x) { boolean absent = !list.contains(x); if (absent) list.add(x); return absent; } }看出问题在哪里了吧?没错,使用了错误的锁,synchronized使用的是this,锁定this可不好用啊,因为操作是在list上发生的!因此应该改用下面的锁定方法:
@ThreadSafe public class ListHelper<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; } } }当然Helper实现的并不优雅,因为在使用两个“逻辑锁”(List自身的this和ListHelper中的list)指向同一个“物理对象锁”。
因此也可以自己将欲拓展的类包装起来:
@ThreadSafe public class ImprovedList<T> implements List<T> { private final List<T> 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; } public synchronized void clear() { list.clear(); } // ... similarly delegate other List methods }4.5 对同步策略进行注释
对各种同步措施,无论是synchronized、volatile还是其他线程安全的class进行注释是非常必要的。
@GuardedBy是不错的一种选择。
然而比较悲剧的是,目前即使Java SE6中的Class,也很少对线程安全进行注释。
一种方法是猜测,或者询问开发者。
另一种方法是,根据他的用途/功能猜测。
例如DataSource为了获取多个Connection,有谁会没事闲的在一个线程反复获取多个Connection呢?显然是为并发设计的,所以应该是线程安全的。
相反,Connection没有理由被多线程共享(SQL查询会被中断!),所以不是线程安全的。
但我觉得最靠谱儿的方法还是拿来去实验一下,跑些高压力的TestCase来看看。