[Java Concurrency in Practice]第四章 对象的组合

对象的组合

我们并不希望每一次内存访问都进行分析以确保程序时线程安全的,而是希望将一些现有的线程安全组件组合为更大规模的组件或程序。本章将介绍一些组合模式,这些模式能够将一个类更容易成为线程安全的,并且在维护这些类时不会无意中破坏类的安全性保证。

4.1 设计线程安全的类

通过使用封装技术,可以使得在不对整个程序进行分析的情况下就可以判断一个类是否时线程安全的。

在设计线程安全类的过程中,需要包含以下三个基本要素: * 找出构成对象状态的所有变量。 * 找出约束状态变量的不变性条件。 * 建立对象状态的并发访问管理策略。

要分析对象的状态,首先从对象的域开始。如果对中所有的域都是基本类型的变量,那么这些域将构成对象的全部状态。如果在对象的域中引用了其他对象,那么该对象的状态就包含被引用对象的域。

同步策略定义了如何在不违背对象不变性条件或后验条件的情况下对其状态的访问操作进行协同。同步策略规定了如何将不可变性、线程封闭与加锁机制等结合起来以维护线程的安全性,并且还规定了哪些变量由哪些锁来保护。要确保开发人员可以对这个类进行分析与维护,就必须将同步策略写为正式文档。

4.1.1 收集同步需求

要确保类的线程安全性,就需要确保它的不变性条件不会在并发访问的情况下被破坏,这就需要对其状态进行推断。对象与变量都有一个状态空间,即所有可能的取值。状态空间越小,就越容易判断线程的状态。final类型的域使用的越多,就能简化对象可能状态的分析过程。(不可变对象只有唯一的状态)

许多类中定义了一些不可变条件,拥有判断状态是有效的还是无效的。long类型的变量,其状态空间为从Long.MIN_VALUE到Long.MAX_VALUE,但Counter中value取值范围存在限制,即不能是负值。

在操作中还会包含一些后验条件来判断状态迁移是否是有效的。如果Counter的当前状态是17,那么下一个有效状态只能是18。当下一个状态需要依赖当前状态时,这个操作就必须是一个复合操作。并非所有的操作都会在状态转换上施加限制,例如,当更新一个保存当前温度的变量时,该变量之前的状态并不会影响计算结果。

由于不变性条件以及后验条件在状态及状态转换上施加了各种约束,因此就需要额外的同步与封装。如果某些状态是无效的,那么必须对底层的状态变量进行封装,否则客户端代码可能会使对象处于无效状态。如果在某个操作中存在无效的状态转换,那么该操作必须是原子的。如果没有施加这种约束,那么就可以放宽封装性或序列化需求,以便获得更高的灵活性或性能。

在类中也可以包含同时约束多个状态变量的不变性条件。在一个表示数值范围的类中可以包含两个状态变量,分别表示范围的上界和下界。这些变量必须遵循的约束是,下界值应该小于或等于上界值。类似于这种包含多个变量的不变性条件将带来原子性需求:这些相关的变量必须在单个原子操作中进行读取或更新。不能首先更新一个变量,然后释放锁并再次获得锁,然后再更新其他的变量。因为在释放锁后,可能会使对象处于无效状态。如果在一个不变性条件中包含多个变量,那么在执行任何访问相关变量的操作时,都必须持有保护这些变量的锁。

如果不了解对象的不变性条件后后验条件,那么就不能确保线程安全性。要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助于原子性与封装性。

4.1.2 依赖状态的操作

类的不变性条件与后验条件月份数了在对象上有哪些状态和转换是有效的。在某些对象的方法中还包含一些基于状态的先验条件。例如,不恩能够从空队列中移除一个元素,在输出元素前,队列必须处于“非空的”状态。如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作。

在单线程程序中,如果某个操作无法满足先验条件,那么就只能失败。但在并发程序中,先验条件可能会由于其他线程执行的操作而变为真。在并发程序中要一直等到先验条件为真,然后再执行该操作。

在Java中,等待某个条件为真得各种内置机制,(包括等待和通知等机制)都与内置加锁机制紧密关联,要想正确地使用他们并不容易。要想实现某个等待先验条件为真时才执行的操作,一种更简单的方法是通过现有库中的类(例如阻塞队列【BlockingQueue】或信号量【Semaphore】)来实现依赖状态的行为。

4.1.3 状态的所有权

如果以某个对象为根节点构造一张对象图,那么该对象的状态将是对象图中所有对象包含的域的一个子集。

在定义哪些变量将构成对象的状态时,只考虑对象拥有的数据。如果分配并填充了一个HashMap对象,那么就相当于创建多个对象:HashMap对象,在HashMap对象中包含的多个对象,以及在Map.Entry中可能包含的内部对象。HashMap对象的逻辑状态包括所有的Map.Entry对象以及内部对象,及时这些对象都是一些独立的对象。

所有权与封装性总是相互关联的:对象封装它拥有的状态,反之也成立,对它封装的状态拥有所有权。状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性。所有权意味着控制权。然而,如果发布了某个可变对象的引用,那么就不再拥有独占的控制权,最多是“共享控制权”。对于从构造函数或者从方法中传递进来的对象,类通常并不拥有这些对象,除非这些方法是被专门设计为转移传递进来的对象的所有权(例如,同步容器封装器的工厂方法)。

容器类通常变现出一种“所有权分离”的形式,其中容器类拥有其自身的状态,而客户代码则拥有容器中各个对象的状态。(容器自身的状态归容器对象控制,put进容器的对象则由客户端代码控制[这些对象要么时线程安全的对象,要么是事实不可变的对象,或者由锁来保护的对象。])

4.2 实例封闭

封装简化了线程安全类的实现过程,它提供了实例封闭机制。当一个对象被封闭到另一个对象中时,能够访问被封闭对象的所有代码路径都是已知。与对象可以由整个程序访问的情况相比,更易于对代码进行分析。通过封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。

将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。

被封闭对象一定不能超出既定的作用域。对象可以封闭在类的一个实例(例如作为类的一个私有成员)中,或者封闭在某个作用域内(例如作为一个局部变量),再或者封闭在线程内(例如在某个线程中将对象从一个方法传递到另一个方法,而不是在多个线程之间共享该对象)。

也只有懂的接受自己的失败,才能更好的去发挥自身优势,也才能够更好的去实现自我;

[Java Concurrency in Practice]第四章 对象的组合

相关文章:

你感兴趣的文章:

标签云: