对象的组合
1 设计线程安全的类
设计线程安全类的过程中,需要包含以下三个基本要素:
- 找出构成对象状态的所有变量
- 找出约束状态变量的不变性条件
- 建立对象状态的并发访问管理策略
对象的状态
- 所有的域都是基本类型,则这些域构成对象的全部状态
- 包含其他对象,该对象的状态将包括被引用对象的域
同步策略
规定了如何将不变性条件、线程封闭和加锁机制结合起来以维护线程的安全性,并且规定了哪些变量由哪些锁来保护
1.1 收集同步需求
final类型的域使用的越多,越能简化对象可能状态的分析过程.
- 不变性条件:判断状态是否是有效的
- 后验条件:判断状态转换是否是有效的
由于上述二条件施加的各种约束,因此就需要额外的同步与封装.
- 如果某些状态是无效的,必须对底层的状态变量进行封装.
1.2 分析依赖状态的操作
- 先验条件:基于状态
依赖状态:包含先验条件的操作
- 单线程程序:无法满足先验条件,只能失败
- 并发程序:先验条件可能因为其他线程的执行而变成真,因此要一直等待先验条件为真再执行该操作
1.3 分析状态的所有权
所有权在Java中只是一个类设计中的要素,在语言层面没有明显的表现.所有权意味着控制权,如果发布了某个可变对象的引用,则意味着共享控制权.在定义哪些变量构成对象的状态时,只考虑对象拥有的数据.
2 实例封闭
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁.
被封闭的对象一定不能超过它们既定的作用域.
对象可以封闭在类的一个实例(eg.私有成员)中,或者封闭在某个作用域内(eg.局部变量),再或者封闭在线程内.
实例封闭是构建线程安全类的一个最简单方式,还使得不同的状态变量可以由不同的锁来保护.
Java的包装器工厂(eg. Collections.synchronizedList.etc),只要包装器对象拥有对底层容器对象的唯一引用(即把底层容器对象封闭在包装器中),那么它就是线程安全的。对底层容器对象的所有访问必须通过包装器来进行。
当发布其他对象时,例如迭代器或内部的类实例,可能会间接地发布被封闭对象,同样会使被封闭对象逸出。
封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就无须检查整个程序
Java监视器模式
遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护.
Java监视器模式的主要优势在于它的简单性.
3 线程安全性的委托
3.1 独立的状态变量
多个变量之间是彼此独立,则可将线程安全性委托给多个状态变量.
即组合成的类不会在其包含的多个状态变量上增加任何不变性条件.
3.2 当委托失效时
如果某个类含有复合操作,那么仅靠委托不足以实现线程安全性。在这种情况下,这个类必须提供自己的加锁机制以保证这些复合操作都是原子操作,除非整个复合操作都可以委托给状态变量。
3.3 发布底层的状态变量
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。
4 在现有的线程安全类中添加功能
4.1 客户端加锁机制
对于由Collections.synchronizedList封装的ArrayList,扩展类的功能,但并不是扩展类本身,而是将扩展代码放入一个”辅助类”中.
如下实现了一个包含”若没有则添加”操作的辅助类,用于对线程安全的List执行操作,但其中的代码是错误的.
@NotThreadSafe
class BadListHelper <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;
}
}
要使其正确执行,必须使List在实现客户端外锁时使用同一个锁.
客户端加锁是指:对于使用某个对象的X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户代码.要使用客户端加锁,必须知道对象X使用的哪一个锁.