我们不希望对每一次的内存访问都进行分析以确保程序是线程安全的,而是希望将一些现有的线程安全组件组合为更大规模的组件或者程序,这里介绍一些组合模式,这些组合模式能够使一个类更容易成为线程安全的,并且在维护这些类时不会无意中破坏类的安全性保证。
1、设计线程安全的类
在设计线程安全类的过程中,需要包含以下三个基本要素:
(1)、找出构成对象状态的所有变量。
(2)、找出约束状态变量的不变性条件。
(3)、建立对象状态的并发访问管理策略。
对象的状态:如果对象中所得的域都是基本类型的变量,那么这些域将构成对象的全部状态;如果在对象的域中引用了其他对象,那么该对象的状态包括被引用对象的域。
1.1 收集同步需求
对象与变量都有 一个状态空间,即所有的可能值。状态空间越小,就越容易判断线程状态。final类型的域使用得越多,就越能简化对象可能状态的分析过程。
在许多类中都定义了一些不可变调条件(某个域的状态范围),用于判断状态是有效的还是无效的。
同样,在操作中还包含一些后验条件判断状态迁移是否是有效的。如果counter当前状态是17,那么下一个状态只能是18.,当下一个状态需要依赖上一个状态时,这个操作必须是复合操作。
由于不变性条件以及后验条件在状态以及 状态转换上施加了各种约束,因此就需要额外的同步和封装。
如果不了解对象的不变性条件和后验条件,那么就不能确保线程安全性。要满足在状态变量的有效值和转换上的各种约束条件,就需要借助于原子性和封装性。
1.2 依赖状态操作
在某些对象的方法中还包含一些基于状态的先验条件,例如不能从空队列中删除一个元素,在删除元素之前,必须得先判断该队列非空。
1.3 状态的所有权
所有权在Java中只是一个设计中的要素,在语言层面没有明显的变现。所有权意味着控制权,如果发布了某个可变对象的引用,则意味着共享控制权。在定义哪些变量构成对象的状态时,只考虑对象拥有的数据。
2、实例封闭
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
被封闭的作用域可以是:
(1)、一个实例中:作为一个私有成员
(2)、某个作用域中:作为局部变量
(3)、线程里:将对象从一个方法传递到另一个方法
2.1、Java监视器模式
从线程封闭原子以及逻辑推论可以得到Java监视器模式。遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象的内置锁来保护。其可变状态都是私有的,并且涉企到该状态的方法都有一个内置锁来保护,而Java的内置锁也称为监视器锁或者监视器。在许多类中都使用了Java监视器模式,例如Vector和Hashtable。
3、线程安全性委托、独立的状态变量
当一个对象有多个状态变量时,即多个域,并且每个状态变量没有耦合性,或者说不相互影响,我们就讲是独立的状态变量。当一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效的状态转换,那么可以将线程安全委托给底层的状态变量,只要每个独立的状态变量是线程安全的,那么整个类就是线程安全的。
假如类的多个状态变量是相互影响的,即使每个状态变量都是线程安全的,那么整个类也有可能不是线程安全的。比如下面的代码:
1 public class NumberRange {
2 // 不变性条件: lower <= upper
3 private final AtomicInteger lower = new AtomicInteger(0);
4 private final AtomicInteger upper = new AtomicInteger(0);
5
6 public void setLower(int i){
7 if(i > upper.get()){
8 throw new IllegalArgumentException("不能设置lower > upper");
9 }
10 lower.set(i);
11 }
12
13 public void setUpper(int i){
14 if(i < lower.get()){
15 throw new IllegalArgumentException("不能设置upper < lower");
16 }
17 upper.set(i);
18 }
19
20
21 }
View Code
NumerRangle不是线程安全的,没有维持对下界和上界进行约束的不变性条件,setLower和setUpper等方法都尝试维护不变性条件,但无法做到setLower和setUpper都是"先检查后执行"的操作,也没有使用加锁机制来维护这些操作的原子性。假如一个线程调用setLower(5),另一个线程调用setUpper(1),那么在一些错误的执行顺序中,两个设置都通过验证了,并且设置成功,结果得到的取值范围就是(5, 1),这是一个无效的状态。虽然分开来讲lower和upper都是线程安全的,但是组合在一起,却不是线程安全的,主要是他们是相互影响的而不是独立的状态变量。因此NumberRangle不能线程安全委托给它的线程安全状态变量。在setLower和setUpper方法中必须加上锁。
4、在现有的线程安全类上添加新的功能
比如我们要给一个链表添加一个新的功能:“若没有则添加”操作,有两个方法,一是客户端加锁机制,二是组合,下面分别介绍这两种方法
4.1 客户端加锁机制
客户端加锁机制主要是将扩展代码放入一个"辅助类"中,如下面代码:
1 public class ListHelper<E> {
2 public List<E> list = (List<E>) Collections.synchronizedCollection(new ArrayList<E>());
3
4 public synchronized boolean putIfAbsent(E x){
5 boolean absent = !list.contains(x);
6 if(absent){
7 list.add(x);
8 }
9 return absent;
10
11 }
12 }
View Code
这中方式仍然不是线程安全的,虽然putIfAbsent方法已经声明为synchronized,但是这个锁和list上的锁是不一样的,不是同一个锁,假如有一个线程正在调用putIfAbsent方法,其他的线程仍然可以对list进行操作,这意味着putIfAbsent方法相对于list的其他操作来说并不是原子性的。
要想putIfAbsent方法能正确执行,必须使List在实现客户端加锁或外部加锁时使用同一个锁,我们对上面的代码进行修改:
1 public class ListHelper<E> {
2 public List<E> list = (List<E>) Collections.synchronizedCollection(new ArrayList<E>());
3
4 public boolean putIfAbsent(E x){
5 synchronized(list){
6 boolean absent = !list.contains(x);
7 if(absent){
8 list.add(x);
9 }
10 return absent;
11 }
12
13 }
14 }
View Code
客户端加锁机制是将扩展的类和基类的实现耦合在一起,正如扩展会破坏实现的封装性,客户端加锁同样破坏了同步策略的封装性。
4.2 组合
当为现有的类添加一个原子操作时,有一个更好的办法:组合(Composition)。如下代码:
1 public class ImprovedList<T> implements List<T> {
2
3 private final List<T> list;
4 public ImprovedList(List<T> list){
5 this.list = list;
6 }
7
8 public synchronized boolean putIfAbsent(E x){
9 boolean absent = !list.contains(x);
10 if(absent){
11 list.add(x);
12 }
13 return absent;
14
15 }
16 public synchronized boolean add(T arg0) {
17 return list.add(arg0);
18 }
19 // ... 按照类似的方式委托list其他方法
20
21 }
View Code
ImprovedList通过自身的内置锁加了一层额外的加锁,并不关系底层的List是否是线程安全的,即使List不是线程安全的或者修改了它的加锁实现,ImprovedList也会提供一致的加锁机制来实现线程安全性。事实上,我们使用了Java监视器模式来封装现有的的List,并且只要在类中拥有指向底层List的唯一外部引用(ImprovedList的构造函数),就能确保线程安全性。