设计线程安全的类
三个基本要素:
1.找出构成对象状态的所有变量
2.找出约束状态变量的不变性条件
3.建立对象状态的并发访问管理策略
收集同步需求
不变性条件包括:
1.不可变条件,如:long类型的变量有效范围为Long.Min到Long.MAX,有些变量可能逻辑上就不能为负数,比如age
2.后验条件:用于判断状态转移是否有效,如count++,当前状态为17,则下一个状态必须为18,当下一个状态依赖当前状态时,这个操作就必须是一个复合操作。
3.先验条件:如,不能从空队列中移除一个元素。如果某个操作中包含基于无效状态的先验条件,那么这个操作就称为依赖状态的操作。
实例封闭
如何通过封闭与加锁等机制使一个类成为线程安全的(即使这个类的状态变量并不是线程安全的)
class PersonSet {
private final Set<Person> mySet = new HashSet<Person>();
public synchronized void addPerson(Person p){
mySet.add(p);
}
public class synchronized boolean containsPerson(Person p){
return mySet.contains(p);
}
}
实例封闭式构建线程安全类的一个最简单方式。,而唯一的访问路径addPerson和containsPerson都加了PersonSet的内置锁(确保该对象只能由一个线程访问,即线程封闭)。所以即使PersonSet的状态由HashSet管理,且HashSet不是线程安全的,但mySet是私有的并且不会逸出,类也是安全的。
java监视器模式
根据实例封闭的逻辑推理我们可以得出监视器模式。(本质就是线程封闭)
原则:把所有的可变状态都封装起来,并由对象自己的内置锁保护
final class Counter {
private long value = 0;
public synchronized long getValue() {
return value;
}
public synchronized long increment() throws IllegalAccessException {
if (value == Long.MAX_VALUE) {
throw new IllegalAccessException("counter overflow");
}
return ++value;
}
}
许多类都使用了java监视器模式,如Vector和Hashtable
线程安全性的委托
我们常常把线程安全性委托给底层的线程安全的工具类,如ConcurrentHashMap
委托失败的情况
class NumberRange{
//不变性条件:lower < upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) throws IllegalAccessException {
//注意:竞态条件,先检查后执行
if( i > upper.get()){
throw new IllegalAccessException("lower不能大于upper");
}
lower.set(i);
}
public void setUpper(int i) throws IllegalAccessException {
//注意:竞态条件,先检查后执行
if( i < lower.get()){
throw new IllegalAccessException("upper不能小于lower");
}
upper.set(i);
}
}
即使NumberRange把所有状态都委托给了线程安全的AtomicInteger封装类,其也不是线程安全的,原因在于他没有维持上下界的不变性条件。假设取值范围为(0,10),如果一个线程调用setLower(5),而另一个调用setUpper(4),那么可能在某个时刻二者都能通过检查,得到取值范围(5,4)。这是个无效的状态,因此必须保证set,get方法的原子性。
在现有的线程安全类中添加功能
class BetterVector<E> extends Vector<E> {
public synchronized boolean putIfAbsent(E x) {//若没有则添加
boolean absent = !contains(x);
if (absent) {
add(x);
}
return absent;
}
继承已有的线程安全类为其添加新的方法
客户端加锁机制(同步代码块)
加了synchronize的方法一定是线程安全的吗?显然答案是否定的
class ListHelper<E>{
public List<E> lis = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E x){//若没有则添加
boolean absent = !list.contains(x);
if(absent){
list.add(x);
}
return absent;
}
}
虽然我们不知道List用哪个锁来保护它的状态,但一定不是ListHelper上的锁,而synchronized方法需要的是ListHelper对象的锁,也就是说二者使用了不同的锁,并不能保证两个操作间是原子的,不能保证一个线程执行putIfAbsent时,另一个线程不修改链表。
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;
}
}
}
使用同步代码块(客户端加锁)来解决这个问题,同步代码块的优势在于他能使加锁的对象更明确(list),防止所错对象的情况发生。
健壮性
健壮性又称鲁棒性,是指软件对于规范要求以外的输入情况的处理能力。所谓健壮的系统是指对于规范要求以外的输入能够判断出这个输入不符合规范要求,并能有合理的处理方式。
软件健壮性是一个比较模糊的概念,但是却是非常重要的软件外部量度标准。软件设计的健壮与否直接反应了分析设计和编码人员的水平。即所谓的高手写的程序不容易死。
组合
当为现有类添加一个原子操作时,以上连个方法都是脆弱的,因为他使加锁代码分布在了不同的类中。为了解决这个问题,一种更好的方法便是组合。
class ImproveList<T> implements List {
private final List<T> list;
ImproveList(List<T> list) {
this.list = list;
}
public synchronized boolean putIfAbsent(T x) {//若没有则添加
boolean contains = list.contains(x);
if (contains) {
list.add(x);
}
return !contains;
}
}
ImproveList将List对象的操作委托给底层的List实例。同时添加一个原子的方法。为所有方法加synchronize,虽然这样做可能会对方法做一些额外的同步导致轻微的性能损失,但是,与其他模式相比这种设计更为健壮,我们并不用关心List底层是否是线程安全的。事实上,我们使用了监视器模式来封装现有的List,只要在类中有指向底层List的唯一外部引用(线程封闭),我们就能保证类的安全性。