组合对象
设计线程安全的类
设计线程安全类的过程应该包括下面3个基本要素:
- 确定对象状态是由哪些变量构成的
- 确定限制状态变量的不变约束
- 制定一个管理并发访问对象状态的策略
对象状态从域讲,如果对象域都是基本类型(primitive)的,那么这些域组成了对象的完整状态。
同步策略定义了对象如何协调对其他状态的访问,并且不会违反它的不变约束后或后验条件。
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;
}
}
}
-
收集同步需求
对象与变量拥有一个状态空间:即它们可能储于的状态范围。
很多类通过不可变约束来判断某一种状态是合法的还是非法的。
操作的后验条件会指出某种状态转换是非法的。
不变约束与后验条件施加在状态及状态转换上的约束,引入了额外的同步与封装的需要。
一个类的不变约束也可以约束多个状态变量。
不理解对象的不可变约束和后验条件,就不能保证线程安全性,要约束状态变量的有效值或者状态转换,就需要原子性与封装性。
-
状态依赖的操作
类的不变约束与方法的后验条件约束了对象合法的状态和合法状态转换。
某些对象的方法也有基于状态的先验条件。
例如: 无法从空队列中移除一个条目:在你删除元素前,队列必须储于“非空”状态。若一个操作存在基于状态的先验条件,则把它称为是状态依赖的。
-
状态所有权
很多情况下,所有权与封装性总是一起出现的:对象封装它拥有的状态,且拥有它封装的状态。拥有给定状态的所有者决定了。
类通常不会拥有由构造函数或方法传递进来的对象,除非该方法是被明确设计用来转换传递对象的所有权的。
容器类通常表现出一种“所有权分离”的形式。
为了防止多线程并发访问同一对象时所带来的干扰,这些对象应该是线程安全对象,高效不可变对象或者由锁明确保护的对象。
实例限制
即使一个对象不是线程安全的,任然由许多技术可以让它安全地用于多线程程序。
通过使用实例限制,封装简化了类的线程安全化工作,通常称为限制。把限制与各种适当的锁策略相结合,可以确保程序以线程安全的方式使用其他非线程安全对象。
将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的值。
被限制对象一定不能逃逸到它的期望可用范围之外。
使用限制确保线程的安全。
@ThreadSafe
public class PersonSet{
@GuardedBy("this")
private final Set<Person> mySet = new HashSet<>();
public synchronized void addPerson(Person p){
mySet.add(p);
}
public synchronzied boolean containsPerson(Person p){
return mySet.contains(p);
}
}
实例限制是构造线程安全类的最简单的方法之一。
限制性使构造线程安全的类变得更容易,因为类的状态被限制后,分析它的线程安全性时,就不必检查完整的程序。
-
Java监视器模式
线程限制原则的直接推论之一是Java监视器模式。遵循Java监视器模式的对象封装了所有的可变状态,并由对象自己的内部锁保护。
// 私有锁保护状态 public class PrivateLock{ private final Object myLock = new Object(); @GuardedBy("myLock