首页 > 代码库 > Java 并发编程(三)设计线程安全的类-实例封闭
Java 并发编程(三)设计线程安全的类-实例封闭
到目前为止,我们已经介绍了关于线程安全与同步的一些基础知识。然而,我们并不希望对每一次内存访问都进行分析以确保是线程安全的,而是希望将一些现有的线程安全组件组合为更大规模的组合为更大规模的组件或程序。之后,我们会讲一些设计线程安全类的一些基本概念,介绍一些组合模式。
一、设计线程安全的类
在设计线程安全类的过程中,需要包含以下三个基本要素:
1、找出构成对象状态的所有变量
2、找出约束状态变量的不变性条件
3、建立对象状态的并发访问管理策略
要分析对象的状态,首先从对象的域开始。如果对象中所有的域都是基本类型的变量,那么这些域将构成对象的全部状态。如果域中引用了其他的对象,那么该对象的状态将包含被引用对象的域。例如,LinkedList 的状态就包含了链表中所有节点对象的状态。
同步策略定义了如何在不违背对象不变条件或后验条件的情况下对状态的访问操作进行协同。同步策略规定了如何将不可变性、线程封闭与加锁机制等结合起来以维护线程的安全性,并且还规定了哪些变量由哪些锁来保护。
1、收集同步需求
要确保类的线程安全型,就要确保它的不变性条件不会在并发访问的情况下被破坏。对象与变量都有一个状态空间,即所有可能的取值。状态空间越小,就越容易判断线程的状态。final 类型的域使用的越多,就越能简化对象可能状态的分析过程。
在许多类中都定义了一些不可变条件,用于判断状态是有效的还是无效的。 比如 long 类型的变量 ,其状态空间为从 Long.MIN_VALUE 到 Long.MAX_VALUE ,或者一些表数量的变量值不能为负值。
同样,在操作中还会包含一些后验条件来判断状态迁移是否有效。 比如变量 counter 当前值为 17,下一个状态只能为18。当下一个状态需要依赖当前状态时,这个操作就必须使一个复合操作。并非所哟肚饿操作都会在状态转换上施加限制。例如,当更新一个保存当前温度的变量时,该变量之前的状态并不会影响计算结果。
由于不变性条件以及后验条件在状态及状态转换上施加了各种约束,因此就需要额外的同步与封装。如果某些状态是无效的,那么必须对底层的状态变量进行封装,否则客户代码可能会使对象处于无效状态。如果在某个操作中存在无效的状态转换,那么该操作必须是原子的。另外,如果在类中没有施加这种约束,那么就可以放宽封装性或序列化等要求,已获得更高的灵活性或性能。
2、依赖状态的操作
类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换是有效地。在某些对象的方法中还包含了一些基于状态的先验条件(Precondition)。例如,不能从空队列中移除一个元素,在删除元素前,队列必须处于“非空的”状态。如果在某个操作中包含有基于状态的先验条件,那么这个操作就成为依赖状态的操作。
3、状态的所有权
许多情况下,所有权与封装性总是相互关联的:对象封装它拥有的状态,反之也成立,即拥有它封装的状态的所有权。状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性。所有权意味着控制权。然而,如果发布了某个可变对象的引用,那么就不再拥有独占的控制权,最多是”共享控制权“。对于从构造函数或者从方法中传递进来的对象,类通常并不拥有这些对象,除非这些方法是被专门设计为转移传递进来的对象的所有权。
容器类通常便显出一种所有权分离的形式,其中容器类拥有自身的状态,而客户代码则拥有容器中各个对象的状态。
二、实例封闭
如果某对象不是线程安全的,那么可以通过多种技术使其可以在多线程程序中安全的被使用。也可以确保该对象只能由单个线程访问(线程封闭),或者通过一个锁来保护对该对象的所有访问。
封装简化了线程安全类的实现过程,它提供了一种实例封闭机制(Instance Confinement),通常也简称为”封闭“。当一个对象被封闭到另一个对象中时,能够访问被封装对象的所有代码路径都是已知的。与对象可以由整个程序访问的情况相比,更容易对代码进行分析。通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。
实力封闭是构建线程安全类的一个最简单方式,他还使得在锁策略的选择上拥有了更多的灵活性。
在 Java 平台的类库中还有很多线程封闭的示例,其中有些类的唯一用途就是将非线程安全的类装化为线程安全的类。一些基本的容器类并非线程安全的,例如 ArrayList 和 HashMap,但类库提供了包装器工厂方法(如 Collections.sychronizedList 及其类似方法),使得这些非线程安全的类可以再多线程环境中安全的使用。这些工厂方法通过”装饰器“(Decorator)模式将容器类风状态一个同步的包装器对象中,而包装器能将接口中的每个方法都实现为同步方法,并将调用请求转发到底层的容器对象上。只要包装器对象拥有对底层容易对象的唯一引用,那么它就是线程安全的。
1、Java 监视器模式
从线程封闭原则及其逻辑推论可以得出 Java 监视器模式。即把对象的所有可变状态都封装起来,并由对象自己的内置锁保护。
许多类中都使用了 Java 监视器模式,例如 Vector 和 HashTable。
Java 监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该所对象,都可以用来保护对象的状态。如 PrivateLock 给出了如何使用私有锁来保护状态。
public class PrivateLock { private final Object myLock = new Object(); Date date; void someMethod(){ synchronized(myLock){ //访问或修改 date 的状态 } } }
使用私有锁对象而不是对象的内置锁,有许多优点。
私有锁对象可以将锁封装起来,使客户代码无法得到锁,但客户代码才可以通过公有方法来访问锁,以便(正确或者不正确的)参与到它的同步策略中。此外,要想验证某个公有访问的锁在程序中是否被正确的使用,则需要检查整个程序,而不是单个类,降低了验证的复杂度。
Java 并发编程(三)设计线程安全的类-实例封闭