第4章 对象的组合

4.1 设计线程安全的类
在线程安全的程序中,虽然可以将程序的所有状态都保存在公有状态域中,但与那些将状态封装起来的程序相比,这些程序的线程安全性更难以得到验证,并且在修改时也更难以始终确保其线程安全性。通过使用封装技术,可以使得在不对整个程序进行分析的情况下就可以判断一个类是否是线程安全的。

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

1.找出构成对象状态的所有变量

2.找出约束状态变量的不变性条件

3.建立对象状态的并发访问管理策略

 

4.1.2 依赖状态的操作

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

 

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

 
4.1.3 状态的所有权
在定义哪些变量将构成对象的状态时,只考虑对象拥有的数据。所有权在Java中并没有得到充分的体现,而是属于类设计中的一个要素。如果分配并填充了一个HashMap对象,那么就相当于创建了多个对象:HashMap对象,在HashMap对象中包含的多个对象,以及在Map.Entry中可能包含的内部对象。HashMap对象的逻辑状态包括所有的Map.Entry对象以及内部对象,即使这些对象都是一些独立的对象。
许多情况下,所有权与封装性总是相互关联的:对象封装它拥有的状态,反之也成立,即对它封装的状态拥有所有权。状态变量的所有者将决定采用何种加锁协议来位置变量状态的完整性。所有权意味着控制权。然而,如果发布了某个可变对象的引用,那么就不再拥有独占控制权,最多是"共享控制权"。对于从构造函数或者从方法中传递进来的对象,类通常并不拥有这些对象,除非这些方法是被专门设计为转移传递进来的对象所有权(例如,同步容器封装器的工厂方法)。
容器类通常表现出一种"所有权分离"的形式,其中容器类拥有自身的状态,而客户代码则拥有容器中各个对象的状态。Servlet框架中的ServletContext就是其中一个示例。ServletContext为Servlet提供了类似于Map形式的对象容器服务,在ServletContext中可以通过名称来注册(setAttribute)或获取(getAttribute)应用程序对象。由Servlet容器实现的ServletContext对象必须是线程安全的,因为它肯定会被多个线程同时访问。当调用setAttribute和getAttribute时,Servlet不需要使用同步,但当使用保存在ServletContext中的对象时,则可能需要使用同步。这些对象由应用程序拥有,Servlet容器知识替代应用程序保管它们。与所有共享对象一样,它们必须安全地被共享。为了防止多个线程在并发访问同一个对象时产生的相互干扰,这些对象应该要么是线程安全的对象,要么是事实不可变的对手,或者由锁来保护的对象。
 
4.2 实例封闭
封装简化了线程安全类的实现过程,它提供了一种实例封闭机制(Instance Confinement)通常也简称"封闭"。当一个对象被封装到另一个对象中,能够访问被封装对象的所有代码路径都是已知的。与对象可以由整个程序访问的情况相比,更易于对代码进行分析。通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用费线程安全的对象。
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
4.2.1 Java监视器模式
遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。
在许多类中都使用了Java监视器模式,例如Vector和Hashtable。Java监视器模式的主要优势在于它的简单性。
public final class Counter{
     @GuardedBy("this") private long value=0;
    public synchronized long getValue(){
        return value;
     }
    public synchronized long increment(){
        if(value == Long.MAX_VALUE) throw new IllegalStateException("counter overflow");
          return ++value;
     }
}

  

使用私有锁对象而不是对象的内置锁(或任何其他科通过公有方式访问的锁),有许多优点。私有的锁对象可以将锁封装起来,使客户代码无法得到锁,但客户代码可以通过公有方法来访问锁,以便(正确或不正确地)参与到它的同步策略中。如果客户代码错误的获得了另一个对象的锁,那么可能会产生活跃性问题。此外,要想验证某个公有访问的锁在程序中是否被正确的使用,则需要检查整个程序,而不是单个的类。
public class PrivateLock {
    private final Object myLock = new Object();
    @GuardedBy("myLock") Widget widget;
    void someMethod(){
          sychronized(myLock){
              //访问或修改Widget的状态
         }
     }
}
 
4.4 在现有的线程安全类中添加功能
4.4.1 客户端加锁机制
对于由Collections.synchronizedList封装的ArrayList,这两种方法在原始类中添加一个方法或者对类进行扩展都行不通,因为客户端代码并不知道在同步封装器工厂方法中返回的List对象的类型。第三种策略是扩展类的功能,但并不是扩展类本身,而是将扩展代码放入一个"辅助类"中。
@NotThreadSafe
public class ListHelper<E>{
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    ...
public synchronized boolean putIfAbsent(E x){
boolean  absent = !list.contains(x);
if(absent) list.add(x);
return absent;
}
}

这种方式不能实现线程安全性。问题在于在错误的锁上进行了同步。无论使用哪一个锁来保护它的状态,可以确定的是,这个锁并不是ListHelper上的锁。ListHelper只是带来了同步的假象。要想使这个方法能正确执行,必须使List在实现客户端加锁或外部加锁时使用同一个锁。

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

在Vector和同步封装器类的文档中指出,它通过使用Vector或封装器的内置锁来支持客户端加锁。

@ThreadSafe
public class ListHelper<E>{
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
...
public boolean putIfAbsent(E x){
synchronized (list){
boolean  absent = !list.contains(x);
if(absent) list.add(x);
return absent;
}
}
}

通过添加一个原子操作来扩展类是脆弱的,因为它将类的加锁代码分布到多个类中,然而客户端加锁更加脆弱,因为它将类C的加锁代码放到与C完全无关的其他类中。

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

 
4.4.2 组合
当为现有的类添加一个原子操作时,组合是更好的方法。
@ThreadSafe
public class ImprovedList<T> implements List<T>{
public final List<E> list;
public ImprovedList<T>(List<E> list){this.list = list}
public synchronized boolean putIfAbsent(T x){
boolean contains = list.contains(x);
if(contains) list.add(x);
return !contains;
}
public synchronized void clear(){list.clear();}
// ... 按照类似的方式委托List的其他方法
}
ImprovedList通过自身的内置锁增加了一层额外的加锁。它并不关心底层的List是否是线程安全的,即使List不是线程安全的或者修改了它的加锁实现,ImprovedList也会提供一直的加锁机制来实现线程安全性。虽然额外的同步层可能导致轻微的性能损失,但与模拟另一个对象的加锁策略相比,ImprovedList更为健壮。

猜你喜欢

转载自mikzhang.iteye.com/blog/2268424
今日推荐