现在的位置: 首页 > 综合 > 正文

Java并发系列(三)线程安全与对象的组合

2013年03月23日 ⁄ 综合 ⁄ 共 5067字 ⁄ 字号 评论关闭
文章目录

Author:Martin

E-mail:mwdnjupt@sina.com.cn

CSDN Blog:http://blog.csdn.net/ictcamera

Sina MicroBlog ID:ITCamera

Main Reference:

《Java并发编程实战》 Brian Goetz etc 童云兰等译

《Java并发设计教程》 温绍锦

大多数对象都是组合对象,如果类中的各个组件都已经是线程安全的,那么这个类是否是线程安全的,我们是否需要再增加一个额外的线程安全层?答案是“视情况而定”,在某些情况下,通过多个线程安全的类组合而成的类是线程安全的,而在某些情况下需要增加额外的机制来保证线程安全。

1.        线程安全性的委托

一般对象的组合过程中会将线程的安全性委托给一个状态变量(或对象),利用这个状态变量的线程安全性来保证线程安全的需要,例如使用同步容器等。

当然很多时候我们也需要将线程的安全性委托给多个状态(或对象),如果这些状态变量彼此独立,即组合而成的类并不会在其包含的多个状态变量上增加任何不变性条件【也就是说如果一个类是由多个独立且线程安全的状态变量组成,并且所有的操作都不包含无效的状态转换,那么可以将线程的安全性委托给这些状态变量】。例如下面的VisualComponent使用CopyOnWriteArrayList来保存各个监听器列表。它是一个线程安全的链表,特别使用于管理监听器列表。每个链表都是线程安全的,此外各个状态之间不存在耦合关心,因此VisualComponent可以将它的线程安全性委托给两个链表:mouseListeners和keyListeners。

publicclass VisualComponent{

       
privatefinal List<KeyListener>  keyListeners   

                                   =new CopyOnWriteArrayList<KeyListener>();

       
privatefinal List<MouseListener>mouseListeners

                                   =new CopyOnWriteArrayList<MouseListener>();

       
publicvoid addKeyListener(KeyListener listener){

           keyListeners.add(listener);

       
}

       
publicvoid addMouseListener(MouseListener listener){

           mouseListeners.add(listener);

       
}

       
publicvoid removeKeyListener(KeyListener listener){

           keyListeners.remove(listener);

       
}

       
publicvoid removeMouseListener(MouseListener listener){

           mouseListeners.remove(listener);

       
}

    }

然而,大多数时候需要将线程的安全性委托给多个状态(或对象),并且不会像上面VisualComponent那么简单,这些状态变量往往是彼此不独立的,因此需要再增加额外的措施或机制来保证线程安全。例如下面的例子。

publicclass NumberRange{

       
// 不变性条件:lower <= upper

       
privatefinal AtomicIntegerlower=new
AtomicInteger(0);

       
privatefinal AtomicIntegerupper=new
AtomicInteger(0);

       
publicvoid setLower(int i){

           //不安全的先检查后执行

           if(i>upper.get()){

               thrownew IllegalArgumentException("lower
> upper!"
);

           }

           lower.set(i);

       
}

       
publicvoid seUpper(int i){

           //不安全的先检查后执行

           if(i<lower.get()){

               thrownew IllegalArgumentException("lower
<upper!"
);

           }

           upper.set(i);

       
}

       
publicboolean isInRange(int i){

           return(i>=lower.get())&&(i<=upper.get());

       
}

    }

NumberRange不是线程安全的,因为没有维持对上下界进行约束的不变性条件。NumberRange可以通过加锁机制来维护不变性条件以确保其他线程的安全性,例如利用一个锁来保护lowerupper,当然前提条件还必须避免发布lowerupper(这个条件已经满足),从而防止客户代码破坏其不变性条件。像NumberRange这种含有复合操作的类仅仅依靠委托并不足以实现线程的安全性,这种情况下必须提供额外的加锁机制以保证这种复合操作是原子操作,除非整个复合操作都可以委托给状态变量。

那么线程安全性委托给某个对象的状态变量(一个或者多个)时,在什么条件下才可以发布这些变量从而时其他类能够修改它们?答案任然取决于在类中对这些变量施加了哪些不变性条件。如果一个状态变量是线程安全的,并且没有任何不变性条件来约束他的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全的发布这个变量。比如上面的VisualComponent例子中,发布keyListeners和mouseListeners是安全的。由于VisualComponent没有在监听器链表的合法状态上施加任何约束,因此这些域可以声明为公有域或者发布,而不会破坏线程的安全性。

2.        现有的安全类中添加功能

Java类库包含许多有用的基础类(基础构建模块-并发构建模块),通常我们应该优先选择重用这些现有的类,而不是建新类,因为重用能降低开发工作量、开发风险和维护成本。有时有某个现有的线程安全类能够支持我们需要的所有操作,但更多的时候现有的类只能支持大部分操作,此时就需要在不破坏线程安全性的情况下添加一个新的操作。例如List中要增加“若没有则添加”的操作,就需要我们根据现有的contain和add方法构造新的操作,并且保证这个操作的原子性。

2.1.        扩展

添加原子性操作最安全的方法是修改原始类,这通常无法做到,因为你可能无法方位或者修改源代码,即使能够修改也必须理解原始代码的同步策略,这样增加的功能才能与原有的设计保持一致。另外一种方法就是扩展,扩展方法很简单,但是原始的类必须向要子类公开状态,如下面所示:

publicclassBetterVector<E>extends
Vector<E>{

       
publicsynchronizedboolean putIfAbsent(E toPut){

           boolean absent=!contains(toPut);

           if(absent){

               add(toPut);

           }

           return absent;

       
}

    }

扩展方法比直接将源代码添加到类中更加脆弱,因为现有的同步策略的实现被分布到多个单独维护的源代码文件中。如果底层类改变了同步策略并选择了不同的锁来保护他的状态变量,那么子类会被破坏,因为不同策略改变后他无法再使用正确的锁来控制对基类状态的并发访问(Vector的规范中定义了他的同步策略,因此BetterVector不存在这个问题)。

2.2.        客户加锁

对于有Collections.synchronizedList封装的ArrayList,上面所述两种方法都行不通,因为客户代码不知道在同步封装器工厂方法返回的List对象类型。第三中方法就是扩展功能,但并不是扩展类本身,而是将扩展代码放到一个辅助类中。这里客户代码是使用对象X的那部分代码,对X而言就是客户代码。“若没有则添加”操作通过客户锁实现如下:

publicclass ListerHelper<E>{

       
public List<E>list=Collections.synchronizedList(new
ArrayList())
;

       
publicboolean putIfAbsent(E toPut){

           synchronized (list){

               boolean absent=!list.contains(toPut);

               if(absent){

                   list.add(toPut);

               }

               return absent;

           }

       
}

    }

这里要注意客户代码使用的锁必须和访问的对象使用同一把锁,即被访问的X的锁和客户的锁是同一锁,这里不能直接将putIfAbsent方法加内置锁,因为这个内置锁是ListerHelper本身的锁。通过扩展来添加原子性操作是脆弱的,因为它将类的加锁代码分布到多个类中,然而客户加锁却更加脆弱,因为类C的代码放到与X完全无关的其他类中去了,这两种方法有共同的特点就是派生类的行为和基类的行为耦合在了一起。

2.3.        组合

为现有的类添加原子操作的一种更好办法就就是组合,例如下面所示:

publicclass ImprovedList<T>implements
List<T>{

       
privatefinal List<T>list;

       
public ImprovedList(List<T> list){

           this.list=list;

       
}

       
publicsynchronizedboolean putIfAbsent(T toPut){

           boolean absent=!list.contains(toPut);

           if(absent){

               list.add(toPut);

           }

           return absent;

       
}

        
publicsynchronizedvoid clear()list.clear();

       
//......按照类似的方式委托list的其他方法

    }

ImprovedList通过自身的内置锁增加了一层额外的加锁。它并不关心底层的list是否线程安全的,即使list不是线程安全的或者修改了他自身的加锁实现,ImprovedList也能提供一致的加锁机制来实现线程安全性。当然额外的同步层可能导致一定的性能损失(性能损失很小,因为底层list上的同步不存在竞争,所以速度很快),但与模拟另一个加锁策略相比,ImprovedList更健壮。事实上,这里使用了Java的监视器模式来封装现有的List,并且只要在类中拥有指向底层List的唯一外部引用,就能确保线程安全性。

2.4.        文档化

在维护线程安全时,文档的是最强大的工具之一。用户可以查阅文档来判断某个类是否是线程安全的,而维护人员也可以通过查阅文档来理解其中的实现策略,避免在维护过程中破坏安全性。然而,通常人们从文档中获得信息确实少之又少,这还是我们亟需加强和改进的的地方。

抱歉!评论已关闭.