对象的组合

Posted 老人与JAVA

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对象的组合相关的知识,希望对你有一定的参考价值。

4 对象的组合

组合模式能够使一个类更容易成为线程安全的,并且在维护这些类时不会无意中破坏类的安全性保证。

4.1设计线程安全的类

设计线程安全类的三个基本要素:

A: 找出构成对象状态的祈有变童。

B: 找出钓束状态变量的不变性条件

C: 建立时象状态的并发访问管理策略

例如,LinkedList的状态就包括该链表中所有节点对象的状态。

 

4.1.1收集同步需求

4.1.2 依赖状态的操作

类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换是有效的。

4.1.3 状态的所有权

如果分配并填充了一个HashMap对象,那么就相当于创建了多个对象:HashMap对象,在HashMap对象中包含的多个对象,以及在Map.Entry中可能包含的内部对象。HashMap对象的逻辑状态包括所有的Map. Entry对象以及内部对象,即使这些对象都是一些独立的对象。

垃圾回收器为我们减少了许多在引用共享方面常见的错误,因此降低了在所有权处理上的开销。

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

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

 

4.2实例封闭

如果某对象不是线程安全的,那么可以通过封装使其在多线程程序中安全地使用。封装简化了线程安全类的实现过程,它提供了一种实例封闭机制,通常也简称为"封闭"。当一个对象被封装到另一个对象中时,能够访问被封装对象的所有代码路径都是已知的。可以将数据的访问跟制在时象的方法上,从而更容易保证线程在访问数据时总是能持有正确的锁

对象可以被封装为全局变量、局部变量、或在一个线程类。但是一定不能超出既定的作用域。

这个示例并未对Person的线程安全性做任何假设,但如果Person类是可变的,那么在访问从PersonSet中获得的Person对象时,还需要额外的同步。要想安全地使用Person对象,最可靠的方法就是使Person成为一个线程安全的类。另外,也可以使用锁来保护Person对象,并确保所有客户代码在访问Person对象之前都已经获得正确的锁。

在Java平台的类库中还有很多线程封闭的示例,其中有些类的唯一用途就是将非线程安全的类转化为线程安全的类。一些基本的容器类并非线程安全的,例如ArrayList和HashMap,但类库提供了包装器工厂方法(例如Collections.synchronizedList及其类似方法),使得这些非线程安全的类可以在多线程环境中安全地使用。

 

4.2.1 监视器模式

监视器模式:自始至终都使用同一个锁保护对象。

Java监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态。程序清单4-3给出了如何使用私有锁来保护状态。

使用私有的锁对象而不是对象的内置锁(或任何其他可通过公有方式访问的锁),有许多优点。私有的锁对象可以将锁封装起来,使客户代码无法得到锁,但客户代码可以通过公有方(someMethod)法来访问锁,以便(正确或者不正确地)参与到它的同步策略中。避免错误地获取到别的锁。

 

4.2.2示例:车辆追踪(详见书P51)

4.3线程安全性的委托

在前面的CountingFactorizer类中,我们在一个无状态的类中增加了一个AtomicLong

类型的域,并且得到的组合对象仍然是线程安全的。由于CountingFactorizer的状态就是

AtomicLong的状态,而AtomicLong是线程安全的,因此CountingFactorizer不会对counter的

状态施加额外的有效性约束,所以很容易知道CountingFactorizer是线程安全的。我们可以说CountingFactorizer将它的线程安全性委托给AtomicLong来保证:之所以CountingFactorizer是线程安全的,是因为AtomicLong是线程安全的。

如果count不是final类型,那么要分析CountingFactorizer的线程安全性将变得更复杂。如果CountingFactorizer将count修改为指向另一个AtomicLong域的引用,那么必须确保count的更新操作对于所有访问count的线程都是可见的,并且还要确保在count的值上不存在竞态条件。这也是尽可能使用final类型域的另一个原因。

4.3.1 独立的状态变量

当然我们还可以将线程安全委托给多个变量。只要这些变量是彼此独立的,即组合而成的类并不会在其包含的不变性条件。例如:监控鼠标和键盘等事件的监听器。它们之间不存在任何关联:

 

4.3.2当委托失效时

大多数组合对象都不会只含有独立状态变量,在它们的状态变量之间存在着某些不变性条件。程序清单4-10中的NumberRange使用了两个AtomicInteger来管理状态,并且含有一个约束条件,即第一个数值要小于或等于第二个数值。

NumberRange不是线程安全的,没有维持对下界和上界进行约束的不变性条件。 NumberRange可以通过加锁机制来维护不变性条件以确保其线程安全性,例如使用一个锁

来保护lower和upper。此外,它还必须避免发布lower和upper,从而防止客户代码破坏其不变性条件。

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

 

4.3.3 发布底层的状态变量

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

例如,发布VisualComponent中的mouseListeners或keyListeners等变量就是安全的。由

于VisualComponent并没有在其监听器链表的合法状态上施加任何约束,因此这些域可以声明为公有域或者发布,而不会破坏线程安全性。

如果将拷贝构造函数实现为this (p.x, p.y),那么会产生竞态条件,而私有构造函数则可以避免这种竞态条。这叫私有构造函数捕获模式

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

要添加一个新的原子操作,最安全的方法是修改原始的类,但这通常无法做到,因为你可能无法访问或修改类的源代码。另一种方法是扩展(实现并重写)这个类。

"扩展"方法比直接将代码添加到类中更加脆弱,如果基类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏。

如下两种解决方案:

4.4.1客户端加锁机制

第三种策略是扩展类的功能,但并不是扩展类本身,而是将扩展代码放人一个"辅助类"中。例如:一个包含"若没有则添加"操作的辅助类,用于对线程安全的List执行操作

必须使List在实现客户端加锁或外部加锁时使用同一个锁。客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户代码。要使用客户端加锁,你必须知道对象X使用的是哪一个锁。(如下是错误的做法

 

总结:

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

客户端加锁机制与扩展类机制有许多共同点,二者都是将派生类的行为与基类的实现籍合在一起。正如扩展会破坏实现的封装性,客户端加锁同样会破坏同步策略的到装性。

 

4.4.2组合

当为现有的类添加一个原子操作时,有一种更好的方法:组合。4-16中的ImprovedList通过将List对象的操作委托给底层的List实例来实现List的操作,同时还添加了一个原子的putIfAbsent方法。(与Collections.synchronizedList和其他容器封装器一样,ImprovedList假设把某个链表对象传给构造函数以后,客户代码不会再直接使用这个对象,而只能通过ImprovedList来访问它。)

ImprovedList通过自身的内置锁增加了一层额外的加锁。它并不关心底层的List是否是线程安全的。InnprovedList提供了一致的加锁机制来实现线程安全性。

 

以上是关于对象的组合的主要内容,如果未能解决你的问题,请参考以下文章

组合(composite)模式

组合模式

设计模式学习笔记(十五:组合模式)

组合模式

设计模式之组合模式

面向对象之组合