在设计线程安全类的过程中,需要包含以下三个基本要素:
1.找出构成对象状态的所有变量
2.找出约束状态变量的不变性条件
3.建立对象状态的并发访问管理策略
4.1.2 依赖状态的操作
如果在某个操作中含有基于状态的先验条件,那么这个操作就称为 依赖状态 的操作。
在Java中,等待某个条件为真的各种内置机制(包括等待和通知等机制)都与内置加锁机制紧密相连,要想正确地使用它们并不容易。要想实现某个等待先验条件为真时才执行的操作,一种更简单的方法是通过现有库中的类(例如阻塞队列[Blocking Queue、Semphore])来实现依赖状态的行为。
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;
}
}
public class PrivateLock {
private final Object myLock = new Object();
@GuardedBy("myLock") Widget widget;
void someMethod(){
sychronized(myLock){
//访问或修改Widget的状态
}
}
}
@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;
}
}
这种方式不能实现线程安全性。问题在于在错误的锁上进行了同步。无论使用哪一个锁来保护它的状态,可以确定的是,这个锁并不是ListHelper上的锁。ListHelper只是带来了同步的假象。要想使这个方法能正确执行,必须使List在实现客户端加锁或外部加锁时使用同一个锁。
客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的的锁来保护这段客户端代码。要使用客户端加锁,就必须知道对象X使用的是哪一个锁。
在Vector和同步封装器类的文档中指出,它通过使用Vector或封装器的内置锁来支持客户端加锁。
@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;
}
}
}
通过添加一个原子操作来扩展类是脆弱的,因为它将类的加锁代码分布到多个类中,然而客户端加锁更加脆弱,因为它将类C的加锁代码放到与C完全无关的其他类中。
客户端加锁机制与扩展类机制有许多共同点,二者都是将派生类的行为与基类的实现耦合在一起。正如扩展会破坏实现的封装性,客户端加锁同样会破坏同步策略的封装性。
@ThreadSafe
public class ImprovedList<T> implements List<T>{
public final List<E> list;
public ImprovedList<T>(List<E> 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();}
// ... 按照类似的方式委托List的其他方法
}
ImprovedList通过自身的内置锁增加了一层额外的加锁。它并不关心底层的List是否是线程安全的,即使List不是线程安全的或者修改了它的加锁实现,ImprovedList也会提供一直的加锁机制来实现线程安全性。虽然额外的同步层可能导致轻微的性能损失,但与模拟另一个对象的加锁策略相比,ImprovedList更为健壮。