三、对象的组合

一、设计线程安全的类

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

在设计线程安全类的过程中,需要包含以下三个基本要素:

  • 找出构成对象状态的所有变量。
  • 找出约束状态变量的不变性条件。
  • 建立对像状态的并发访问管理策略。

要分析对象的状态,首先从对象的域开始。如果对象中所有的域都是基本类型的变量,那 么这些域将构成对象的全部状态。于含有n个基本类型域的对象,其状态就是这些域构成的n元组。 例如,二维点的状态就是它的坐标值(x, y)。如果在对象的域中引用了其他对象,那么该对象 的状态将包含被引用对象的域。例如,LinkedList的状态就包括该链表中所有节点对象的状态。

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

  • 收集同步需求

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

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

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

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

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

  • 依赖状态的操作

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

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

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

  • 状态的所有权

在定义哪些变量将构成对象的状态时,只考虑对象拥有的数据。所有权(Ownership) 在Java中并没有得到充分的体现,而是属于类设计中的一个要素。如果分配并填充了一个 HashMap对象,那么就相当于创建了多个对象:HashMap对象,在HashMap对象中包含的多 个对象,以及在Map.Entry中可能包含的内部对象。HashMap对象的逻辑状态包括所有的Map. Entry对象以及内部对象,即使这些对象都是一些独立的对象。

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

容器类通常表现出一种“所有权分离”的形式,其中容器类拥有其自身的状态,而 客户代码则拥有容器中各个对象的状态。Servlet框架中的ServletContext就是其中一个示 例。ServletContext为Servlet提供了类似于Map形式的对象容器服务,在ServletContext中 可以通过名称来注册(setAttribute)或获取(getAttribute)应用程序对象。由Servlet容器 实现的ServletContext对象必须是线程安全的,因为它肯定会被多个线程同时访问。当调用 setAttribute和get Attribute时,Servlet不需要使用同步,但当使用保存在ServletContext中的对 象时,则可能需要使用同步。这些对象由应用程序拥有,Servlet容器只是替应用程序保管它们|。 与所有共享对象一样,它们必须安全地被共享。为了防止多个线程在并发访问同一个对象时产 生的相互干扰,这些对象应该要么是线程安全的对象,要么是事实不可变的对象,或者由锁来 保护的对象。

二、实例封闭

如果某对象不是线程安全的,那么可以通过多种技术使其在多线程程序中安全地使用。 你可以确保该对象只能由单个线程访问(线程封闭),或者通过一个锁来保护对该对象的所有访问。

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

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

被封闭对象一定不能超出它们既定的作用域。对象可以封闭在类的一个实例(例如作为类 的一个私有成员)中,或者封闭在某个作用域内(例如作为一个局部变量),再或者封闭在线 程内(例如在某个线程中将对象从一个方法传递到另一个方法,而不是在多个线程之间共享该 对象)。当然,对象本身不会逸出——出现逸出情况的原因通常是由于开发人员在发布对象时 超出了对象既定的作用域。

封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全 性时就无顼检查整个程序。

  • Java监视器模式

从线程封闭原则及其逻辑推论可以得出Java监视器模式。遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。

在许多类中都使用了 Java监视器模式,例如Vector和Hashtable。在某些情况下,程序需 要一种更复杂的同步策略。

Java监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态。

public class PrivateLock {

    private final Object myLock = new Object();

    Widget widget;

    public void someMethod() {
        synchronized(myLock) {
            // 访问或修复Widget的状态
        }
    }
}
三、线程安全性的委托

大多数对象都是组合对象。当从头开始构建一个类,或者将多个非线程安全的类组合为一个类时,Java监视器模式是非常有用的。但是,如果类中的各个组件都已经是线程安全的,会是什么情况呢?我们是否需要再增加一个额外的线程安全层?答案是“视情况而定”。在某些情况下,通过多个线程安全类组合而成的类是线程安全的),而在某些情况下,这仅仅是一个好的开端)。

  • 独占的状态变量

我们可以将线程 安全性委托给多个状态变量,只要这些变量是彼此独立的,即组合而成的类并不会在其包含的 多个状态变量上增加任何不变性条件。

如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包 含无效状态转换,那么可以#线程安全性委托给底层的状态变量。

如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这变量。

四、在现有的线程安全类中添加功能

Java类库包含许多有用的“基础模块”类。通常,我们应该优先选择重用这些现有的 类而不是创建新的类:重用能降低开发工作量、开发风险(因为现有的类都已经通过测试) 以及维护成本。有时候,某个现有的线程安全类能支持我们需要的所有操作,但更多时候,现有的类只能支持大部分的操作,此时就需要在不破坏钱程安全性的情况下添加一个新的 操作。

  • 客户端加锁机制

对于由Collections.synchronizedList封装的ArrayList,这两种方法–在原始类中添加一个方法或者对类进行扩展都行不通,因为客户代码并不知道在同步封装器工厂方法中返回的List对象的类型。第三种策略是扩展类的功能,但并不是扩展类本身,而是将扩展代码放入一个“辅助类”中。

  • 组合

当为现有的类添加一个原子操作时,有一种更好的方法:组合(Composition)。

public class ImprovedList<T> implements List<T> {


    private final List<T> list;

    public ImprovedList(List<T> list) {
        this.list = list;
    }

    public synchronized boolean putlfAbsent(T x) {
        boolean contains = list.contains(x);
        if (contains)
            list.add(x);
        return !contains;
    }

    public synchronized void clear() {

    }
	
	...............
}	

五、将同步策略文档化

在维护线程安全性时,文档是最强大的(同时也是最未被充分利用的)工具之一。用户可 以通过査阅文档来判断某个类是否是线程安全的,而维护人员也可以通过査阅文档来理解其中的实现策略,避免在维护过程中破坏安全性。
在文档中说明客户代码需要了解的线程安全性保证,以及到吗维护人员需要了解的同步策略。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

书香水墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值