Java并发编程学习——《Java Concurrency in Practice》学习笔记 4.对象的组合

14 篇文章 0 订阅

4.1 设计线程安全的类

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

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

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

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

4.1.1 收集同步需求

要确保类的线程安全性,就需要确保它的不变性条件不会再并发访问的情况下被破坏。对象与变量所有可能的取值称作状态空间,状态空间越小,判断线程的状态就越容易。final域越多,就越能简化对象可能状态的分析过程。

如果对象的下一个状态依赖于当前的状态,状态更新操作就必须是一个复合操作。并非所有的操作都会在状态转换上施加限制。

状态转换的过程中如果存在无效的状态转换,那么该操作必须是原子的。

4.1.2 依赖状态的操作

如果在某个操作中包含有基于状态的 先验条件 Precondition ,那么这个操作就称为依赖状态的操作。

要想实现某个等待先验条件为真时才执行的操作,较简便的方法是通过现有库中的类,如 阻塞队列 Blocking Queue 或 信号量 Semaphore

4.1.3 状态的所有权

在定义哪些变量将构成对象的状态时,只考虑对象拥有的数据。

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

容器类通常表现出一种“所有权分离”的形式,其中容器类拥有其自身的状态,而客户端代码则拥有容器中各个对象的状态。

4.2 实例封闭

封装简化了线程安全类的实现过程,它提供了一种 实例封闭机制 Instance Confinement。当一个对象被封装到另一个对象中时,能够访问被封装对象的所有代码路径都是一致的。通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。

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

被封闭的对象一定不能超出它们既定的作用域。

使用实例封闭确保线程安全的例子

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);
    }

}

*如果Person是可变的,那么在访问从PersonSet中获得的Person对象时,还需要额外的同步。

完整代码

实例封闭是构件线程安全类的一个最简单方式

实例封闭还使得不同的变量可以由不同的锁来保护。

4.2.1 Java监视器模式

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

监视器模式的典型示例

public class Counter {

    private long value = 0;

    public synchronized long getValue() {
        return value;
    }

    public synchronized long increament() {
        if (value == Long.MAX_VALUE) {
            throw new IllegalStateException("counter overflow");
        }
        return ++value;
    }

}

完整代码

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

使用私有锁保护对象状态

public class PrivateLock {

    private final Object myLock = new Object();

    Widget widget;

    void someMethod() {
        synchronized (myLock) {
            // 访问或修改Widget的状态

        }
    }

}

完整代码

使用私有的锁对象而不是对象的内置锁,或任何其他可通过共有方式访问的锁,有更多的额优点。因为锁被封装了,客户无法得到。不会产生活跃性问题。

4.3 线程安全的委托

在某些情况下,通过多个线程安全类组合而成的类是线程安全的,但在某些情况下不是。

4.3.2 独立的状态变量

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

4.3.3 多个状态变量

如果某个类含有复合操作,那么仅靠委托并不足以实现线程安全性。在这种情况下,这个类必须提供自己的加锁机制以保证这些复合操作都是原子操作,除非整个复合操作都可以委托给状态变量。

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

4.3.4 发布底层的状态变量

当把线程安全性委托给某个对象的底层状态变量时,在什么条件下可以发布这些变量从而使其他类能够修改它们,取决于在类中对这些变量施加了哪些不变性条件。

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

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

要添加一个新的原子操作,最安全的方法是修改原始类,单着通常无法做到。另一种方法是扩展这个类,假定在设计这个类时考虑了可扩展性。“扩展”方法比直接将代码添加到类中更加脆弱,因为同步策略实现被分布到多个单独维护的源代码文件中。底层类同步策略的改变会导致子类被破坏。

4.4.1 客户端加锁机制

客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户代码。要使用客户端加锁,必须知道对象X使用的是哪一个锁。

通过添加一个原子操作来扩展类是脆弱的,客户端加锁更加脆弱。因为它将类C的加锁代码放到类C完全无关的其他类中。当在哪些并不承诺遵循加锁策略的额类上使用客户端加锁时,要特别小心。

4.4.2 组合

组合 Composite 是为现有类添加原子操作的更好的方法。

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

    private final List<T> list;

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

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

    @Override
    public synchronized boolean add(T arg0) {
        return list.add(arg0);
    }

    // ...
}

完整代码

4.5 将同步策略文档化

维护线程安全性时,文档时最强大的工具之一。在文档中应当说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略

应该保证将类中的线程安全性文档化。
- 是否线程安全
- 执行回调时是否持有一个锁
- 是否有某些特定的锁会影响其行为
- 可以不指向支持客户端加锁,但要明确指出
- 如果希望客户代码能够在类中添加新的原子操作,那么需要在文档说明需要获得哪些锁才能实现安全的原子操作
- 如果使用锁来保护状态,要将其写入文档以便日后维护

如果某个类没有明确的声明是线程安全的,那么就不要假设它是线程安全的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值