对象的组合
在设计线程安全类的过程中,需要包含以下三个基本要素:
1. 找出构成对象状态的所有变量
2. 找出约束状态变量的不变性条件
3. 建立对象状态的并发访问管理策略
- 当对象的下一个状态需要依赖当前状态时,这个操作就必须是一个复合操作。如有一个Counter类,当前状态为17,那么下一个状态只能是18
- 相关变量的读取和更新必须在单个原子操作中进行
- 如果某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作。如删除一个队列里的元素,这个队列当前必须是“非空”的
- 封闭机制更易于构造线程安全的类
1.Java监视器模式
遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护
以下代码是对象使用私有锁的例子:
public class PrivateLock {
private final Object myLock = new Object();
public void doSomething() {
synchronized(myLock) {...}
}
}
2.线程安全性的委托
委托是创建线程安全类的一个有效手段:只需让现有的线程安全类管理所有的状态即可。
通过多个线程安全类组合而成的类,其线程安全性视情况而定。
- 对象中有单个线程安全对象是线程安全的
- 如果一个类是由多个独立且线程安全的状态变量组成的,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量
- 如果类中多个变量之间存在着一定的不变性条件,那么这个类就不是线程安全的,需要采取同步机制
- 如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在不允许的状态转换,那么就可以安全地发布这个变量
3.在现有的线程安全类中添加功能
要在现有的类中添加线程安全操作,有两种方法:
- 修改原始类的代码
- 扩展这个类。一下代码扩展了Vector
public class BetterVector<E> extends Vector<E> {
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if (absent) {add(x);}
return absent;
}
}
扩展的方法比直接修改源代码要脆弱,因为同步策略实现被分布到了多个单独维护的源代码文件中。如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏。
- 客户端加锁机制
客户端加锁机制是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户代码。要使用客户端加锁机制,必须知道对象X使用的是哪一个锁。如下代码中,第一个实现版本使用了不同的锁,所以其并不是原子的。
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
//verson 1
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent) list.add(x);
return absent;
}
//verson 2
public boolean putIfAbsent(E x) {
synchronized(list) {//使用list自己的锁
boolean absent = !list.contains(x);
if (absent) list.add(x);
return absent;
}
}
}
- 组合
为现有类添加一个原子操作时,更好的方法是:组合(现有类作为对象的私有final域)
4.将同步策略文档化
在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略