这里介绍一下组合模式。编写线程安全的类的一个重要的思路就是,将一些现有的线程安全组件组合为更大规模的组件或者程序。在设计线程安全类的过程中,需要包含以下三个基本要素:
1.找出构成对象状态的所有变量。
2.找出约束状态变量的不变性条件。
3.建立对象状态的并发访问管理策略。
收集同步需求
要确保类的线程安全性,就需要确保它的
不变性条件不会在并发访问的情况下被破坏。同样,在操作中还会包含一些
后验条件来判断状态迁移是否是有效的。当下一个状态需要依赖当前状态时,这个操作就必须是一个复合操作。如果不了解对象的不变性与后验条件,那么就不能确保线程安全性。要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助于原子性与封装性。
依赖状态的操作
在某些对象的方法中还包含一些基于状态的
先验条件,如删除前的非空判断,这样的操作就被称为依赖状态的操作。要想实现某个等待先验条件为真时才执行的操作,一种简单的方法是通过现有库中的类,如阻塞队列Blocking Queue或者信号量Semaphore来实现依赖状态的行为。
状态的所有权
对象封装它拥有的状态,反之也成立,即对它封装的状态拥有所有权。状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性。
实例封闭
封装简化了线程安全类的实现过程,它提供了一种实例封闭机制,通常也简称为“封闭”。当一个对象被封装到另一个对象中时,能够访问被封装对象的所有代码路径都是已知的。将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
Java监视器模式
遵循Java监视器模式的对象会把对象所有可变状态都封装起来,并由对象自己的内置锁来保护。Java监视器模式仅仅是一种编码的约定,对于任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态。
public class PrivateLock{
private final Object myLock = new Object();
@GuardedBy("myLock")
Widget widget;
void someMethod(){
synchronized (myLock) {
//访问或修改Widget的状态
}
}
}
使用私有锁对象的优点是,可以将锁封装起来,使客户代码无法得到锁。
线程安全性的委托
如果一个类中只存在单个线程安全的状态变量,那么其线程安全性可以委托给该线程安全的状态变量。对于多个状态变量的情况,如果
多个线程安全的状态变量彼此之间是相互独立的,那么也可以将线程安全性委托给它们,反之如果多个状态变量之间不是独立的,那么就不能将线程安全性委托给线程安全状态变量。
在现有的线程安全类中添加功能
主要有两种方式,在客户端加锁,但要确保使加锁对象在实现客户端加锁或者外部加锁时使用同一个锁。
@ThreadSafe
public class ListHelper<E>{
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E x){
synchronized(list){//使List在实现客户端加锁或者外部加锁时使用同一个锁
boolean absent = !list.contains(x);
if(absent)
list.add(x);
return absent;
}
}
}
另外一个更好的方式是使用组合
@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;
}
}
将同步策略文档化
在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。