- 将状态封装起来的程序比将所有状态保存在共有静态域的程序更容易验证线程安全性.
设计线程安全类的过程中,需要包含以下三个基本要素:
- 找出构成对象状态的所有变量
- 找出约束状态变量的
不变性条件
- 建立对象状态的
并发访问管理策略
-
对于含有N个基本类型域的对象,其状态就是这些域构成的N元组
-
同步策略(Synchronization Policy)
定义了如何在不违背对象不变性条件
或后验条件
的情况下对其状态的访问操作进行协同.同步策略规定了如何将不可变性
,线程封闭
与加锁机制
等结合起来以维护线程的安全性,并且还规定了哪些变量由哪些锁来保护
要确保开发人员可以对这个类进行分析与维护,就必须将同步策略写为正式文档
-
要确保类的线程安全性,就需要确保它的
不变性条件
不会在并发访问的情况下被破坏,这就需要对其状态进行推断 -
许多类中都定义了一些不
可变条件
,用于判断状态是否有效(eg:取值范围是否有效)
在操作中还会包含一些后验条件
来判断状态迁移
是否有效(eg:状态改变后的值是否合法)
如果不了解对象的
不变性条件
与后验条件
,那么就不能确保线程安全性.要满足在状态变量的有效状态转换上
的各种约束条件,就需要借助与原子性
与封装性
-
类的
不变性条件
与后验条件
约束了在对象上有哪些状态和状态转换是有效的.在某些对象的方法中还包含一些基于状态的先验条件
如果在某个操作中包含基于状态的先验条件
,那么这个操作就称为依赖状态的操作
(eg: 空队列中不能移除元素) -
在并发程序中要一直等到
先验条件
为真,然后才执行该操作. -
要想实现某个等待
先验操作
为真才执行的操作,更简单的方法是通过现有库中的类
-
所有权与封装性总是相互关联的:
对象封装它拥有的状态,拥有它封装的状态所有权
状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性 -
如果发布了某个可变对象的引用,那么就不再拥有对其独占的控制权,最多是
共享控制权
-
容器类通常表现出一种"所有权分离"的形式,容器类拥有其自身的状态,客户端代码则拥有容器中各个对象的状态.
- 封装简化了线程安全类的实现过程,它提供了一种
实例封闭机制(Instance Confinement)
当一个对象被封装到另外一个对象中时,能够访问被封装对象的所有代码路径都是已知的.
与对象可由整个程序访问相比,更易于对代码进行分析
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁
-
被封闭对象一定不能超出它们既定的作用域.对象可封闭在类的一个实例(对象私有成员),某个作用域(就不变量)或线程内.
-
发布一个本该封闭的对象会破坏
封闭性
发布其他对象(迭代器或内部类实例),也可能会间接地发布被封闭对象,造成其逸出
封闭机制
更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性
时就无须检查整个程序
-
遵循
Java监视器模式
的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护. -
Java监视器模式
仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态.//通过一个私有锁来保护状态 public class PrivateLock{ private final Object myLock = new Object(); Widget widget; void someMethod(){ synchronized(myLock){ //访问或修改Widget的状态 } } }
-
使用私有锁对象的好处:
私有的锁对象可以将锁封装起来,使客户端代码无法得到锁,但客户端代码可以通过方法来访问锁,以便(正确或不正确)参与到它的同步策略中
如果客户端代码错误地获取到另一个对象的锁,那么可能产生活跃性问题
-
在某些情况下,通过多个线程安全类组合而成的类是线程安全的
而在某些情况下,这仅仅是一个好的开端. -
不可变的值可以被自由地共享与发布
- 可以将线程安全性
委托
给多个状态变量,只要这些变量是彼此独立的,即组合而成的类并不会在其包含的多个状态变量上增加任何不变性条件
- 如果组合对象的状态变量之间存在着某些不变性条件(复合操作),那么仅靠
委托
并不足以实现线程安全性
这种情况下,这个类必须提供自己的加锁机制以保证这些复合操作
都是原子操作
,除非整个复合操作
都可以委托
给状态变量
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性
委托
给底层状态变量
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量
public class BetterVector<E> extends Vector<E>{
public synchronized boolean putIfAbsent(E x){
boolean absent = !contains(x);
if(absent)
add(x);
return absent;
}
}
- "扩展"方法比第一种(修改原始类)更加脆弱,因为
同步策略实现
被分不到多个单独维护的源代码文件中.
如果底层改变了同步策略
并选择了不同的锁来保护它的状态变量,那么子类会被破坏,因为在同步策略
改变后它无法再使用正确的锁来控制对基类状态的并发访问
.
-
扩展类的功能(并不是扩展类本身,而是将扩展代码放入"辅助类"中)
//通过客户端代码加锁 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; } } }
-
通过添加一个原子操作来扩展类是脆弱的,因为它的加锁代码分不到多个类中.
然而,通过客户端加锁
更加脆弱,因为它将一个类的加锁代码放到与这个类完全无关的其他类中.
-
通过将将List对象的操作
委托
给底层List对象实例来实现List的操作,同时添加新的原子操作
.public class ImprovedList<T> implements List<T>{ private final List<T> list; public 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; } public synchronized void clear(){ list.clear(); } //.... 按照类似操作委托List的其他方法 }
-
虽然额外的同步可能导致轻微的
性能损失
,但与模拟另一个对象的加锁策略
相比,组合更为健壮.
- 在维护线程安全时,文档是最强大的工具之一.
用户通过查阅文档来判断某个类是否线程安全的,而维护人员也可以通过查阅文档来理解其中的实现策略,避免在维护过程中破坏安全性.
在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略