Java并发编程实战读书笔记——第四章 对象的组合

第4章 对象的组合

将一些现有的线程安全组件组合为更大规模的组件或程序。

4.1 设计线程安全的类

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

  1. 找出构成对象状态的所有变量
  2. 找出约束状态变量的不变性条件
  3. 建立对象状态的并发访问管理策略
4.1.1 收集同步需求

状态空间越小,就容易判断线程的状态。final类型的域使用得越多,就越能简化对象可能状态的分析过程。

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

4.1.2 依赖状态的操作

类的不变性条件与后验条件约束了对象上有哪些状态和状态转换是有效的。在某些对象的方法中还包含一些基于状态的先验条件,删除先判断非空状态,那么这个操作就称为依赖状态的操作。

在Java中,等待某个条件为真的各种内置机制都与内置加锁机制紧密关联。要想实现某个等待先验条件为真时才执行的操作,一种更简单的方式是通过现有库中的类,BlockingQueue或者Semaphore以及其它同步工具类。

4.1.3 状态的所有权

对象的状态是对象图中所有对象包含的域的一个子集。

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

无论如何,垃圾回收机制使我们避免了如何处理所有权的问题。

许多情况下,所有权与封装性总是相互关联的:对象封装它拥有的状态,反之也成立,对它封装的状态拥有所有权。状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性。如果发布了某个可变对象的引用,那么就不再拥有独占的控制权。

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

例:ServletContext为Servlet提供了类似于Map形式的对象容器服务,在ServletContext中可以通过名称来注册setAttribute或获取getAttribute应用程序对象。由Servlet容器实现的ServletContext对象必须是线程安全的,因为它肯定会被多个线程同时访问。当调用setAttribute和getAttribute时,不需要同步,但当使用保存在ServletContext中的对象时,刚可能需要同步。

4.2 实例封闭

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

被封闭对象一定不能超出它们既定的作用域。对象可以封闭在类的一个实例中,或者封闭在某个作用域内,再或者封闭在线程内。

PersonSet说明了如何通过封闭与加锁等机制使一个类成为线程安全的。

这里写图片描述

实例封闭是构建线程安全类的一个最简单方式,它还使得在锁策略的选择上拥有了更多的灵活性。在PersonSet中使用了它的内置锁来保护它的状态,但对于其他形式的锁来说,只要使用同一个锁就可以保护状态。实例封闭还使得不同的状态变量可以由不同的锁来保护。

在Java平台的类库中还有很多线程封闭的示例,其中有些类的唯一用途就是将非线程安全的类转化为线程安全的类。一些基本的容器类例如ArrayList不是线程安全的,但类库提供了包装器工厂方法,例如Collections.synchronizedList及其类似方法,使得这些非线程安全的类可以在多线程环境中安全地使用。这些工厂方法通过”装饰器Decorator”模式将容器封装在一个同步的容器对象上,而包装器能将接口中的每个方法都实现为同步方法,并将调用请求转发到底层的容器对象上。只要包装器对象拥有对底层容器对象的唯一引用(即把底层容器对象封闭在包装器中),那么它就是线程安全的。在这引起方法的Javadoc中指出,对底层容器对象的所有访问必须通过包装器来进行。

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

4.2.1 Java监视器模式

Counter中封闭了一个状态变量value,对该变量的所有访问都需要通过Counter的方法来执行,并且这些方法都是同步的。Java监视器模式的主要优势就在于简单性。例如Vector和Hashtable。
当从头开始构建一个类,或者将多个非线程安全的类组合为一个类时,Java监视器模式是非常有用的。

进入和退出同步代码块的字节指令也称为monitorenter和monitorexit,而Java的内置锁也称为监视器锁或监视器。

4.3线程安全性的委托

之所以CountingFactorizer是线程安全的,是因为AtomicLong是线程安全的。

4.3.1 基于委托的车辆追踪器

使用ConucrrentHashMap和不可变的Point类

4.3.2 独立的状态变量

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

使用CopyOnWriteArrayList来保存各个监听器列表。

4.3.3 当委托失效时

当状态变量之间存在着某些不变性条件,使用两个AtomicInteger来管理状态,并且包含一个约束条件,即第一个数值要小于或等于第二个数值。

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

4.3.4 发布底层的状态变量

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

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

要添加一个新的原子操作,最安全的方法就是修改原始的类

另一种方法是扩展这个类,需要状态向之类公开。扩展方法比直接将代码添加到类中更加脆弱,因为现在的同步策略实现被分配到多个单独维护的源代码文件中。如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被在破坏。

4.4.1 客户端加锁机制

这里写图片描述

客户端加锁却更加脆弱,因为它将类C的加锁代码放到与C完全无关的其他类中。客户端加锁与扩展类机制有许多共同点,二者都是将派生类的行为与基类的实现耦合在一起。正如扩展会破坏实现的封装性,客户端加锁同样会破坏同步策略的封装性。

4.4.2 组合

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

这里写图片描述

ImporvedList通过自身的内置锁增加了一层额外的加锁。它并不关心底层的List是否是线程安全的,即使List不是线程安全的或者修改了它的加锁实现,ImporvedList也会提供一致的加锁机制来实现线程安全性。虽然额外的同步层可能导致轻微的性能损失,但与模拟另一个对象的加锁策略相比,ImprovedList更为健壮。事实上,我们使用了Java监视器模式来封装现有的List,并且只要在类中拥有指向底层List的唯一外部引用,就能确保线程安全性。

4.5 将同步策略文档化

在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。

许多正式的Java技术规范,例如Servlet和JDBC,也没有在它们的文档中给出线程安全性的保证和需求。

由容器代替Web应用程序来保存这些属性应该是线程安全的,或者是不可变的。

从Servlet应该程序代码访问它们时,应该确保它们始终由同一个锁保护。但由于容器可能需要序列化HttpSession中的对象以实现复杂或钱化操作,并且容器不可能知道你的加锁协议,因此你要自己确保这些对象是线程安全的

DataSource.getConnection()是线程安全的。大多数应用程序在实现使用JDBC Connection对象的操作时,通常都会把Connection对象封闭在某个特定的线程中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值