并发编程原则与技术(四)——组合对象

 
并发编程原则
(四)
组合对象
  言:
   之前的系列文章中我们讨论了编写线程安全程序的原则与技术。但是作为我们日常的开发,总是想非常快速的编写出高效安全的多线程程序。因此我们不想每一次都去分析内存的访问、锁的竞争等等一系列问题,而是希望基于线程安全组件来构造出更大的线程安全组件。或者我们可能还会遇到扩展一个线程安全类的情况。本篇文章我们将会讨论一些模式,这些模式会帮助我们更好的完成上述任务,而不会使我们的组合和扩展破坏原有的线程安全性。
1 )、设计线程安全类:
   日常开发中一定要在我们的头脑中形成线程安全类的印象,只有知道如何设计线程安全类、如何识别线程安全类,我们才能在组合与扩展中不至于丢失来之不易的线程安全。总的来说设计线程安全类要包括以下 3 个基本要素:
   . 确定对象状态是由哪些变量构成
   . 确定限制状态变量的不变约束
   . 确定管理对象状态的并发访问策略
先说对象状态,如果对象的状态是由基本类型元素组成,那么对象状态就是这些基本类型元素,如果对象状态中还包括其他对象的引用,那么对象状态还包括所引用对象的状态。在操作对象状态时,一定要识别以下这些情况:
1、        不变约束的维护:正如我们之前文章中讨论的不变约束的概念,当不变约束涉及多个状态变量时,任何一个操作在访问这些状态变量的时,必须占有保护这些变量的锁。
2、        识别状态依赖操作:
如果对一个对象状态的操作依赖于另一个对象的变化,那么这个操作称为状态依赖操作。在 Java 中处理状态依赖操作可以使用 wait/notify 操作,这项技术是与 Java 内置锁邦定在一起的。在 Java5.0 中又出现了一些新的技术,如:阻塞队列,信号量对象等。
3、        注意对象所有权的变化:
所有权意味着控制权,一般情况下对象封装它所拥有的状态,并且拥有它所封装状态的控制权。如果该对象是不可变对象或者对象没有被发布,那么对象的状态为该对象所独占,但是一旦该对象被发布到一个可变对象上,那么该对象状态的控制权将由该对象和那个可变对象所共享。
2 )、实现线程安全的方法与模式:
   1 、实例限制:
     即使对象不是线程安全的,也可以通过很多方法使它运行在线程安全环境下。把一个对象封装在一个对象内部,并把访问封装对象数据限制在对象方法上称为实例限制,这更易于确保线程访问数据时总是能够获取正确的锁。把限制与各种适当的锁策略相结合,可以确保程序以安全的方式使用其他非线程安全对象。为了防止不必要的逸出,可以将对象限制在类实例中、语汇范围内(如:本地变量)、或者线程中。如下面代码 4.2.1 所示:
public class PersonSet{
 private final Set<Person> myset=new HashSet<Person>();
 public synchronized void adperson(Person p){
   myset.add(p);
 }
 public synchronized boolean containperson(Person p){
   return myset.contains(p);
 }
}
在上面的代码中虽然 myset 并非线程安全对象,但是在 PersonSet 类中 myset 没有非法逸出,而且所有对公共对象 myset 的操作都是由同一对象的内部锁保护的,因此可以保证获取 person 对象是线程安全的,但是上面的代码无法保证对 Person 对象的操作是安全的,如果想保证对 person 的操作线程安全,那么要么使用额外的锁来保护,要么使 Person 成为线程安全类。线程限制最直接的例子就是 Java 监视器模式,遵守 Java 监视器模式的对象封装了所有可变的状态,并且由对象自己的内部锁保护。如下面 4.2.2 代码所示,该代码描述了一个汽车位置的动态追踪器,使用一个字符串唯一标识汽车,并且使用一个坐标标识汽车位置:
 /**
    标识汽车位置坐标的类
 **/
 public class MutablePoint{
public int x,y;
public MutablePoint(){x=0;y=0}
public MutablePoint(MutablePoint p){
 this.x=p.x;
 this.y=p.y;
}
 }
 public class MonitorVehicleTracker{
private final Map<String,MutablePoint> locations;
public MonitorVehicleTracker(Map<String,MutablePoint> locations){
 this.locations=deepcopy(locations);
}
public synchronized Map<String,MutablePoint> getLocations(String id){
 return deepcopy(locations);
}
public synchronized MutablePoint getLocation(String id){
 MutablePoint loc=locations.get(id);
 return loc==null?null:new MutablePoint(loc);
}
public synchronized void setLocation(String id,int x,int y){
 MutablePoint loc=locations.get(id);
 if(loc==null){
    throw new IllegalArgumentException(“No such ID:”+id);
 }
 loc.x=x;
 loc.y=y;
}
private static Map<String,MutablePoint>
deepcopy(Map<String,MutablePoint> m){
Map<String,MutablePoint> result=new HashMap< String,MutablePoint>();
for(String id:m.keySet(){
 result.put(id,new MutablePoint(m.get(id)));
}
 return result;
    }
 }
上面的代码中虽然 MutablePoint 类不是线程安全的,但是 MonitorVehicleTracker 类确是线程的,而且 MonitorVehicleTracker 类没有将 Map 或它的任何可变点发布出去。当需要获得任何有关汽车位置的信息时,所返回的信息是通过相应的 clone 机制( deepcopy 方法),从底层数据源拷贝了一份相同的可变数据作为返回数据。先复制可变数据在返还给用户,这种实现方式可以部分维护线程安全性。但是这种方式有一个副作用,就是可能造成源数据与返回数据的不一致,即使源数据已经改变,所返回的数据仍然保持不变。这是好是坏取决于你的需要,如果你不关心数据的实时变化,那么这就无所谓;但是如果你关心数据的实时变化,那么你就要采用其他一些辅助手段来不时的对刷新返回的数据与源数据保持同步,这通常使用事件监听以及辅助线程来实现。
2 、委托线程安全:
   有些时候我们可以充分利用 Java 类库中为我们提供的线程安全类来构建我们自己的线程安全类。比如我们可是使用 JDK1.4 以及以前版本中的 Vector,HashTable 类或者通过 synchronizedMap 等方法将某个容器对象包装成线程安全容器;在 JDK1.5 中可供我们选择的组件更加丰富,比如原子变量(如 AtomicInteger )、并发容器(如: ConcurrentMap )等等。因此我们可以利用委托模式,将类的线程安全性委托于线程安全组件,由这些组件去完成并发操作。但是这里有一个问题,那就是在委托线程安全时要注意类的不变约束限制。也就是说,在操作对象多个状态时,不能破坏这些状态之间的约束,如果一个操作涉及对多个状态的改变,要么使用一个统一的锁来保护操作这些状态的全过程,要么使用一些监控机制,一旦违背了不变约束即刻失败并抛出异常。如下面代码 4.2.3 所示:
public class NumberRange{
 private final AtomicInteger lower=new AtomicInteger();
 private final AtomicInteger upper=new AtomicInteger();
 public void setLower(int i){
if(i>upper.get()){
 throw new IllegalArgumentException(“lower > upper”);
}
lower.set(i);
 }
 public void setUpper(int i){
if(i<lower.get()){
 throw new IllegalArgumentException(“upper< lower ”);
}
upper.set(i);
 }
}
上面代码中的线程安全性委托给了原子变量,由原子变量负责并发操作的线程安全。而且每一次的状态改变都对不变约束进行了检查,如果发现违背不变约束马上操作失败并抛出异常。
3 )、扩展线程安全类:
如果我们想向一个线程安全类添加一个新方法来扩展它,那我们一定要多加小心,因为往往我们在不经意间的改变,就会破坏已有的线程安全。通常扩展线程安全类主要有客户端加锁以及组合对象两种方法。客户端加锁主要是通过使用一个额外的锁,施加于一个已有的类之上,来保证新添加的方法的线程安全性,这通常是通过构建某个类的助手类来实现。如下面的代码 2.4.5 所示,通过构建 List 的助手类 ListHelper 来添加一个 putIfAbsent 方法:
public class ListHelper{
 public List<E> list=Collections.synchronizedList(new ArrayList());
 public boolean putIfAbsent(E x){
     synchronized(list){
       boolean absent=list.contains(x);
       if(!absent){
         list.add(x);
       }
        return absent;
     }
 }
}
通过添加一个额外的锁施加于 list 之上,来保证新方法对 list 操作的线程安全性。另一个向已有类中添加原子操作的更加健壮的方法是组合对象。其实组合对象方法是使用装配器模式来实现的,如下面 2.4.6 代码所示:
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 absent=list.contains(x);
if(!absent){
 list.add(x);
}
return absent;
 }
}
上面代码中一旦一个 List 传递到构造函数中,客户将不再直接使用这个 List ,而是使用被包装后的 ImprovedList 来实现操作。 ImprovedList 引入了一个新的锁层,它并不关心传递来的 List 是否线程安全,他都提供了自己的锁来保证线程操作的安全性。但是如果传递进来的 List 是线程安全的,这个额外的同步锁层次可能会造成一些性能损失。
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值