对象的组合
主要介绍一些组合模式,使得一个类更容易成为线程安全的类,在维护这些类时不会无意中破坏类的安全性保证。
1.设计线程安全的类
设计线程安全类的过程中,需要包含以下三个基本要素:
- 找出构成对象状态的所有变量;
- 找出约束状态变量的不变性条件;
- 建立对象状态的并发访问管理策略。
对象的状态
- 如果所有的域都是基本类型,则这些域构成对象的全部状态;
- 如果包含其他对象,该对象的状态将包括被引用对象的域。
同步策略规定了如何将不变性条件、线程封闭和加锁机制结合起来以维护线程的安全性,并且规定了哪些变量由哪些锁来保护。
1.1 收集同步需求
**尽量多的使用final域。**final类型的域使用的越多,状态空间就越小,越能简化对象可能状态的分析过程。
在单个或多个域上,某一个操作如果存在的无效的状态转换,需要对该操作进行同步。无效的状态转换包括不满足:
- 不变性条件,例如:int变量超出最大最小值。
- 后验条件,例如:counter当前值是17,那么下一个操作结束一定是18.
如果某个操作存在无效的状态转换,那么该操作必须是原子的,即需要同步。
1.2 分析依赖状态的操作
某些方法包含一些先验条件才能执行,例如:不能够从空队列中删除一个值。单线程程序中如果遇到无法满足先验条件的情况可以直接返回失败,但是并发程序中先验条件可能因为其他线程的执行而变成真,因此要一直等待先验条件为真再执行。
1.3 分析状态的所有权
所有权在Java中只是一个设计中的要素,在语言层面没有明显的变现。所有权意味着控制权,如果发布了某个可变对象的引用,则意味着共享控制权。在定义哪些变量构成对象的状态时,只考虑对象拥有的数据。
2.同步的实现
同步的实现手段主要有三个:
* 对象封闭
* 委托线程安全性给底层状态变量
* 现有类的封装
2.1 对象封闭
当一个对象封闭在另一个对象中时,由于被访问的对象所有代码路径都是已知的,因此更易于对代码进行分析。
被封闭的作用域可以是:
- 一个实例中:作为一个私有成员
- 某个作用域中:作为局部变量
- 线程里:将对象从一个方法传递到另一个方法
通过封闭机制确保线程安全
@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);
}
需要注意的是,在这个实现中,需要Person也是一个线程安全的类,否则一样会线程不安全。也需要注意,不应该让一个本该封闭的对象逸出作用域。
Java中很多容器比如ArrayList和HashMap不是线程安全的类,但类库通过Collections.synchronizedList及类似方法产生一个容器的“装饰器”,使其访问变成了线程安全的。
2.1.1 Java监视器模式
遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由自己的内置锁保护。
使用监视器模式
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;
}
}
Counter中封装了一个变量value,对该变量的所有访问都需要通过Counter的方法执行,这些方法都是同步的。
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.2 线程安全性委托
如果一个类是由多个独立且线程安全的状态变量组成,所有操作中都不会破坏已有的限制条件,那么可以将线程安全性委托给底层的状态变量去做。如果某个类含有复合操作,即多个状态变量之间相互影响,仅仅委托给底层的状态变量去做就不足以保证线程安全性,需要在该类中加锁以保证线程安全性。
发布底层的状态变量 如果一个状态变量是线程安全的,并且没有任何不变性条件约束他的值,也不存在任何不允许的状态转换,那么就可以安全的发布这个变量。例如Counter,value需要是正值(约束条件)且必须递增(如果发布客户可以减少,存在不允许的状态转换),因此不能安全的发布Counter中的value状态。
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里面的keyListeners和mouseListeners发不出去是安全的。因为没有在其监听器列表的合法状态上施加任何约束,这些域发布不会破坏线程安全。
2.3 给现有的类中增加功能
给现有的类增加新的功能并保持线程安全性有两种方式。
- 客户端加锁
- 将加锁代码分布在多个类中,破坏同步策略的封装性。
- 组合
以给一个list增加putIfAbsent功能为例:
客户端加锁
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;
}
}
}
组合方式
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;
}
// Plain vanilla delegation for List methods.
// Mutative methods must be synchronized to ensure atomicity of putIfAbsent.
...
}
本文介绍了设计线程安全类的基本原则,包括确定对象状态、分析依赖状态的操作和所有权等。同时探讨了实现同步的多种方法,如对象封闭、委托线程安全性给底层状态变量以及现有类的封装。

被折叠的 条评论
为什么被折叠?



