》》设计线程安全的类
@@ 通过使用封装技术,可以使得在不对整个程序进行分析的情况下就可以判断一个类
是否是线程安全的。
@@ 在设计线程安全类的过程中,需要包含以下三个基本要素:
---------- 找出构成对象状态的所有变量。
--------- 找出约束状态变量的不变性条件
---------- 建立对象状态的并发访问管理策略
@@ 要分析对象的状态,首先从对象的域开始。如果对象中所有的域都是基本类型的变量,
那么这些域将构成对象的全部状态。
@@ 如果在对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域。
@@ 同步策略定义了如何在不违背对象不变条件或后验条件的情况下对其状态的访问操作
进行协同。
同步策略规定了如何将不可变性、线程封闭与加锁机制等结合起来以维护线程的安全
性,并且还规定了哪些变量由哪些锁来保护。
要确保开发人员可以对这个类进行分析与维护,就必须将同步策略写为正式文档。
### 收集同步需求
@@ 对象与变量都有一个状态空间,即所有可能的值。状态空间越小,就越容易判断线程
的状态。final 类型的域使用得越多,就越能简化对象可能状态的分析过程。(在极端的情况
下,不可变对象只有唯一的状态)。
@@ 在许多类中都定义了一些不可变条件,用于判断状态是有效的还是无效的。
@@ 在操作中还包含一些后验条件来判断状态迁移是否是有效的。
@@ 由于不变性条件以及后验条件在状态及状态转换上施加了各种约束,因此就需要额外的
同步与封装。
如果某些状态是无效的,那么必须对底层的状态变量进行封装,否则客户代码可能会使
对象处于无效状态。
如果某个操作中存在无效的状态转换,那么该操作必须是原子的。
另外,如果在类中没有施加这种约束,那么就可以放宽封装性或序列化等需求,以便获得
更高的灵活性或性能。
@@ 在类中也可以包含同时约束多个状态变量的不变性条件。
@@ 包含多个变量的不变性条件将带来原子性需求:这些相关的变量必须在单个原子操作中
进行读取或更新。
@@ 如果在一个不变性条件中包含多个变量,那么在执行任何访问相关变量的操作时,都必须
持有保护这些变量的锁。
@@ 如果不了解对象的不变性条件与后验条件,那么就不能确保线程安全性。要满足在状态
变量的有效值或状态转换上的各种约束条件,就需要借助于原子性与封装性。
### 依赖状态的操作
@@ 类的不变性条件与后验条件约束了在对象上哪些状态与状态转换是有效的。
@@ 如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作。
@@ 在单线程程序中,如果某个操作无法满足先验条件,那么就只能失败。但在并发程序中,
先验条件可能会由于其他线程执行的操作而变成真。在并发程序中要一直等到先验条件为真然后
再执行该操作。
@@ 在 Java 中,等待某个条件为真的各种内置机制(包括等待和通知机制)都与内置锁紧密
关联,要想正确地使用它们并不容易。
要想实现某个等待先验条件为真时才执行的操作,一种更简单的方法是通过现有库中的类
(例如阻塞队列【Blocking Queue】 或 信号量【 Semaphore】)来实现依赖状态的行为。
### 状态的所有权
@@ 在定义哪些变量将构成对象的状态时,只考虑对象拥有的数据。
@@ 所有权在 Java 中并没有得到充分的体现,而是属于类设计中的一个要素。
@@ 无论如何,垃圾回收机制使我们避免了如何处理所有权的问题。
------------- 在 Java 中存在所有权模型,只不过垃圾回收机器为我们减少了许多在引用共享方面
常见的错误,因此降低了在所有权处理上的开销。
@@ 许多情况下,所有权与封装性总是相互关联的:对象封装它拥有的状态,反之也成立,即
对它封装的状态拥有所有权。
状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性。
所有权意味着控制权。然而,如果发布了某个可变对象的引用,那么就不再拥有独占的控
制权,最多是 “ 共享控制权 ” 。
对于从构造函数或者从方法中传递进来的对象,类通常并不拥有这些对象,除非这些方法
是被专门设计为转移传递进来的对象的所有权(例如,同步容器封装器的工厂方法)
@@ 容器类通常表现出一种 “ 所有权分离 ” 的形式,其中容器类拥有其自身的状态,而客户
代码则拥有容器中各个对象的状态。
@@ Servlet 框架中的 ServletContext
---------- 由 Servlet 容器实现的 ServletContext 对象必须是线程安全的,因为它肯定会被多个
线程同时访问。当调用 setAttribute 和 getAttribute 时, Servlet 不需要使用同步,但当
使用保存在 ServletContext 中的对象时,则可能需要使用同步。
@@ 为了防止多个线程在并发访问同一个对象时产生的相互干扰,这些对象应该要么是线程
安全的对象,要么是事实不可变的对象,或者由锁来保护的对象。
》》实例封闭
@@ 封装简化了线程安全类的实现过程,它提供了一种实例封闭机制,通常也简称为“ 封闭” 。
@@ 通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程
安全的对象。
@@ 将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程
在访问数据时总能持有正确的锁。
@@ 被封闭对象一定不能超出它们既定的作用域。对象可以封闭在类的一个实例(例如作为类
的一个私有成员)中,或者封闭在某个作用域内(例如作为一个局部变量),再或者封闭在线程
内(例如在某个线程中将对象从一个方法传递到另一个方法,而不是在多个线程之间共享该对象)。
@@ 示例:通过封闭机制来确保线程安全
-------------------------------------------------------------------------------------------------------------------------------------
@ThreadSafe
public class PersonSet{
private final Set<Person> mySet = new HashSet<Person> ( ) ;
public synchronized void addPerson ( Person p ){
mySet.add(p);
}
public synchronized boolean containsPerson( Person p ){
return mySet.contains(p) ;
}
}
-----------------------------------------------------------------------------------------------------------------------------------
补充:(1)、上面的示例并未对 Person 的线程安全做任何假设,但如果 Person 类是可变的,
那么在访问从 PersonSet 中获得的 Person 对象时,还需要额外的同步。
(2)、要想安全地使用 Person 对象,最可靠的方法就是使 Person 成为一个线程安全
的类。另外,也可以使用锁来保护 Person 对象,并确保所有客户代码在访问 Person
对象之间都已经获得正确的锁。
@@ 实例封闭是构建线程安全类的一个最简单方式,它还使得在锁策略的选择上拥有了更多的
灵活性。
@@ 实例封闭还使得不同的状态变量可以由不同的锁来保护。
@@ 在 Java 平台的类库中还有很多线程封闭的示例,其中有些类的唯一用途就是将非线程安全
的类转换为线程安全的类。
@@ 一些基本的容器类并非线程安全的,例如 ArrayList 和 HashMap ,但类库提供了包装器
工厂方法(例如 Collections.synchronizedList 及其类似方法),使得这些非线程安全的类可以
在多线程环境中安全地使用。这些工厂方法通过 “装饰器” 模式将容器封装在一个同步的包装器
对象中,而包装器能将接口中的每个方法都实现为同步方法,并将调用请求转发到底层的容器
对象上。
只要包装器对象拥有对底层对象的唯一引用(即把底层容器对象封闭在包装器中),那么它
就是线程安全的。
在这些方法的 javadoc 中指出,对底层容器对象的所有访问必须通过包装器来进行。
@@ 封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就
无须检查整个程序。
### Java 监视器模式
@@ 遵循 Java 监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来
保护。
@@ 在许多类中都使用了 Java 监视器模式,例如 Vector 和 Hashtable 。
@@ Java 监视器模式的主要优势就在于它的简单性。
@@ Java 监视器模式仅仅是一个编写代码的约定,对于任何一种锁对象,只要自始至终都使用
该锁对象,都可以用来保护对象的状态。
@@ 示例:通过一个私有锁来保护状态
------------------------------------------------------------------------------------------------------------------------------------
public class PrivateLock {
private final Object myLock = new Object ( ) ;
@guardedBy("myLock") Widget widget ;
void someMethod( ){
synchronized ( myLock ){ // 私有锁
// 访问和修改 Widget 的状态
}
}
}
----------------------------------------------------------------------------------------------------------------------------------
补充:(1)、使用私有的锁对象而不是对象的内置锁(或任何其他可通过公有方式访问的锁),
有许多优点。
--------- 私有的锁对象可以将锁封装起来,使得客户代码无法得到锁,但客户代码可以
通过公有方式来访问锁,以便(正确或者不正确地)参与到它的同步策略中。
如果客户代码错误地获得了另一个对象的锁,那么可能会产生活跃性问题。
(2)、 要想验证某个公有访问的锁在程序中是否被正确地使用,则需要检查整个程序,
而不是单个的类。
### 示例:车辆追踪
@@ 视图线程与执行更新操作的线程将并发地访问数据模型,因此该模型必须是线程安全的。
》》线程安全性的委托
@@ 大多数对象都是组合对象。当从头开始构建一个类,或者将多个非线程安全的类组合为
一个类时,Java 监视器模式是非常有用的。但是,如果类中的各个组件都已经是线程安全的,
我们是否需要再增加一个额外的线程安全层? 这个得 “ 视情况而定 ” 。
### 示例:基于委托的车辆追踪器
### 独立的状态变量
@@ 可以将线程安全性委托给多个状态变量,只要这些变量是彼此独立的,即组合而成的类
并不会在其包含的多个状态变量上增加任何不变性条件。
@@ CopyOnWriteArrayList 是一个线程安全的链表,特别适用于管理监听器列表。
### 当委托失效时
@@ 如果某个类含有复合操作,那么仅靠委托并不足以实现线程安全性。在这种情况下,
这个类必须提供自己的加锁机制以保证这些复合操作都是原子操作,除非整个复合操作都可以
委托给状态变量。
@@ 如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效
状态转换,那么可以将线程安全性委托给底层的状态变量。
### 发布底层的状态变量
@@ 当把线程安全性委托给某个对象的底层状态变量时,在什么条件下可以发布这些变量从而
使其他类能修改它们?
答案: 取决于在类中对这些变量施加了哪些不变性条件。
@@ 如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上
也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。
### 示例:发布状态的车辆追踪器
》》在现有的线程安全类中添加功能
@@ Java 类库包含许多有用的 “ 基础模块 ” 类。通常,我们应该优先选择重用这些现有的类
而不是创建新的类:重用能降低开发工作量、开发风险(因为现有的类都已经通过测试)以及
维护成本。
@@ 要添加一个新的原子操作:
---------- 法一(最安全的方法):修改原始的类
修改原始的类,这通常无法做到,因为你可能无法访问或修改类的源代码。要想修改原始
的类,就需要理解代码中的同步策略,这样增加的功能才能与原有的设计保持一致。如果直接将
新方法添加到类中,那么意味着实现同步策略的所有代码仍然处于一个源代码文件中,从而更容
易理解与维护。
------------ 法二:扩展这个类(假定在设计这个类时考虑了可扩展性) -----》 extends
补充:
“ 扩展 ” 方法比直接将代码添加到类中更加脆弱,因为现有的同步策略被分布到了多个单独
维护的源代码文件中。
如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏,
因为在同步策略改变后它无法再使用正确的锁来控制对基类状态的并发访问。
----------- 法三:扩展类的功能,但并不是扩展类本身,而是将扩展代码放入一个 “ 辅助类 ” 中
下面要介绍的客户端加锁(辅助类)
---------- 法四: 组合
### 客户端加锁机制
@@ 客户端加锁是指,对于使用某个对象 X 的客户端代码,使用 X 本身用于保护其状态的锁来保护
这段客户代码。要使用客户端加锁,你必须知道对象 X 使用的是哪一个锁。
@@ 客户端加锁比扩展类更加脆弱。
---------- 当在那些不承诺遵循加锁策略的类上使用客户端加锁时,要特别小心。
@@ 客户端加锁机制与扩展类机制有许多的共同点
---------- 二者都是将派生类的行为与基类的实现耦合在一起。
--------- 扩展会破坏实现的封装性
客户端加锁会破坏同步策略的封装性
### 组合
》》将同步策略文档化
@@ 在维护线程安全性时,文档是最强大的(同时也是最未被充分利用的)工具之一。
用户可以通过查阅文档来判断某个类是否是线程安全的,而维护人员也可以通过查阅文档
来理解其中的实现策略,避免在维护过程中破坏安全性。
@@ 在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的
同步策略。
@@ synchronized 、 volatile 或者任何一个线程安全类都对应于某种同步策略,用于在并发
访问时确保数据的完整性。这种策略是程序设计的要素之一,因此应该将其文档化。
@@ 在设计同步策略时需要考虑多个方面,例如,将哪些变量声明为 volatile 类型,哪些变量
用锁来保护,哪些锁保护哪些变量,哪些变量必须是不可变的或者被封闭在线程中的,哪些操作
必须是原子操作等。其中某些方面是严格的实现细节,应该将它们文档化以便日后的维护。还有
一些方面会影响类中加锁行为的外在表现,也应该将其作为规范的一部分写入文档。