首页 > 代码库 > 设计线程安全的类--对象的组合

设计线程安全的类--对象的组合

通过一些组合模式能够使一个类更容易成为线程安全的,并且易于维护。避免复杂的内存分析来确保线程是安全的。

设计一个线程安全的类要报案下面三个要素:

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

    对象的所有域构成了对象的状态。如果对象的域是基本变量构成,那么这些域构成了对象的全部状态。如果对象的域中引用了其他对象,那么对象的状态也包含其引用对象的域。如ArrayList的状态就包含其所有节点对象的状态。

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

  3、建立对象状态的并发访问策略。

    将不变性,线程封闭,加锁机制等结合起来维护线程的安全性。

 

收集同步需求

  要确保线程安全,就需要确保不变性条件不会再并发访问中被破坏。对象和变量都有一个状态空间,即所有可能的取值。状态空间越小越容易判断线程状态。

public final class Counter{
    private long value = http://www.mamicode.com/0;
    public synchronized long getValue(){
        return value;
    }

    public synchronized long increment(){
        if(value =http://www.mamicode.com/= Long.MAX_VALUE)
            throw new IllegalStateException();
        return ++value;
    }
}

  许多类中都定义了不可变条件,用于判断状态是否有效。如Counter中的value是long类型的变量,取值范围是Long.MIN_VALUE到Long.MAX_VALUE,但是还有一个限制,就是value不能是负值。

  类中也存在中后验条件类判断状态的迁移是否有效。如counter的当前状态是17,那么下一个有效状态就是18。当下一个状态依赖当前状态,那么这个操作就是符合操作。并非soy状态转换操作都有后验条件,比如更新温度时,并不依赖前一个结果。

  由于不变性条件和后验条件在状态和状态转换上施加了约束,因此需要额外的同步和封装,否则客户端代码可能是对象处于无效状态。如果存在无效的状态转换,那么必须是原子操作。

  对于包含多个状态变量的不变性条件,需要在原子操作中进行读取和更新,不能首先更新一个变量然后释放锁,然后更新其他变量。因为释放锁后,可能是对象处于无效状态。

  要确保对象的线程安全,必须要了解对象的不变性条件和后验条件,需要借助原子性和封装性。

  先验条件(Pre-condition),后验条件(Post-condition),不变性条件(Invariant)含义如下:

  1. Pre-conditions are the things that must be true before a method is called. The method tells clients "this is what I expect from you".
  2. Post-conditions are the things that must be true after the method is complete. The method tells clients "this is what I promise to do for you".
  3. Invariants are the things that are always true and won‘t change. The method tells clients "if this was true before you called me, I promise it‘ll still be true when I‘m done".

依赖状态的操作

  某些对象的方法中包含一些基于状态的先验条件,不能从一个空队列中移除一个元素。在单线程中,无法满足先验条件,操作会失败。但是在并发程序中,因为其他线程的操作使得原本不满足先验条件的操作成功执行。

实例封闭

  对于一个非线程安全的对象可以通过一些技术使其在多线程中安全使用。如,确保单线程访问该对象,或者通过锁来保护该对象。

  通过封装可以简化线程安全类的实现过程,它提供了一种实例封闭机制。当一个对象被封装到另一个对象中时,所有访问该对象的代码都是已知的,相比被整个程序访问的对象,被封装的对象更容易分析。被封闭的对象一定不能超出既定的作用域。

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

public class PersonSet{
    private final Set<Persion> mySet = new HashSet<>();
    public synchronized long addPersion(Persion p){
        mySet.add(p);
    }

    public synchronized boolean containsPersion(Persion p){
        return mySet.contains(p);
    }
}

  HashSet并非线程安全的,但是HashSet被封闭在PersionSet中,唯一能访问mySet的代码都由锁保护的。因此PersionSet的线程安全的。

  本例中并没有假设Persion的线程安全性。如果Persion是可变的,那么访问persion还需要额外的同步。

  JAVA的类库中也有很多类似的线程封闭的类,如Collections.synchronizedList及其类似方法,这些类的唯一用途就是将非线程安全的类转换成线程安全的类。

 

Java监视器模式

  java的内置锁被称为监视器锁或监视器,将所有可变对象封装起来,并有对象自己的内置锁保护。属于实例封闭。前面的Counter示例就是这种模式。也可以通过私有锁来保护对象,可以避免客户端代码获取锁,产生死锁问题,而且只需检查单个类就可以验证锁是否正确使用。

示例:车辆追踪

class MutablePoint{
    public  int x,y;
    public MutablePoint(){
        x=0;
        y=0;
    }
    public MutablePoint(MutablePoint point){
        this.x = point.x;
        this.y = point.y;
    }

}

public class MonitorVehicleTracker{
    private final Map<String, MutablePoint> locations;
    public MonitorVehicleTracker(Map<String, MutablePoint> locations){
        this.locations = locations;
    }

    public synchronized Map<String, MutablePoint> getLocations(){
        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 IllegalStateException();
        loc.x = x;
        loc.y = y;
    }

    private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m){
        Map<String, MutablePoint> locs = new HashMap<>();
        for(String id:m.keySet()){
            MutablePoint loc = new MutablePoint(m.get(id));
            locs.put(id, loc);
        }
        return Collections.unmodifiableMap(locs);
    }
}

 

  假设每辆车都有一个String对象来标记,同时拥有一个位置坐标(x,y)。通过一个线程读取位置,将其显示出来,vehicles.getLocations()

  其他线程负责更新车辆的位置。vehicles.setLocation(id, x, y);

  由于存在并发访问,必须是线程安全的,因此使用了监视器模式,确保了线程的安全。尽管MutablePoint不是线程安全的,但是可变的Point并没有被发布。当返回车辆位置时,通过deepCopy方法来复制当前的位置。因此MonitorVehicleTracker是线程安全的。通过复制可变数据类维持线程安全。可能存在一些问题,如性能问题,不能实时反映车辆位置,因为返回的是快照。

线程安全性的委托

  如果类中的各个组件都是线程安全的,那么是否还需要额外的线程安全层?需要看情况。在某些情况下,通过线程安全类组合而成的类是线程安全的,称之为线程安全性的委托。

  将车辆追踪器的实例改变下,代码如下:

class Point{
    public final int x,y;
    public Point(int x, int y){
        this.x=x;
        this.y=y;
    }

}

public class MonitorVehicleTracker{
    private final ConcurrentHashMap<String, Point> locations;
    private final Map<String, Point> unModifiableMap;

    public MonitorVehicleTracker(Map<String, Point> locations){
        this.locations = new ConcurrentHashMap<>(locations);
        unModifiableMap = Collections.unmodifiableMap(this.locations);
    }

    public Map<String, Point> getLocations(){
        return unModifiableMap;
    }

    public void setLocation(String id, int x, int y){
        if(locations.replace(id, new Point(x, y)) == null)
            throw new IllegalStateException();
    }
}

  我们只是将最初的可变MutablePoint类变成不可变的Poient,不可变的值可以自由的分享和发布,因此返回的locattion不需要复制。使用了线程安全的ConcurrentHashMap来管理,因此没有使用显示的同步,同时确保了线程安全。将线程安全委托给ConcurrentHashMap。

委托给独立的状态变量

  上面我们都是委托给单个线程安全的状态变量。我们也可以委托给多个状态变量,但是这些变量必须是彼此独立的,即组合后的类在多个状态变量上没有任何不变性条件。

委托失效

  大多数组合对象存在着某些不变性条件。会导致委托失效,非线程安全。

public class NumberRange{
    //lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

    public void setLower(int i){
        if(i > upper.get())
            throw new IllegalArgumentException();
        lower.set(i);
    }
    public void setUpper(int i){
        if(i < lower.get())
            throw new IllegalArgumentException();
        upper.set(i);
    }
    public boolean isInRange(int i){
        return (i >= lower.get()) && i <= upper.get();
    }
}

  NumberRange不是线程安全的,因为进行了先检查后执行操作,并且这个操作不是原子性的,破坏了上下界进行约束的不变性条件。setLower和setUper都尝试维持不变条件,但是失败了。我们可以通过加锁机制来维护不变性条件来确保线程安全性。因此类似的符合操作,仅靠委托无法实现线程安全。

  如果一个状态变量是线程安全的,并且没有任何不变性条件来约束,也不存在无效的状态转换,n那么就可以安全的发布这个变量。

 

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

  对一个线程安全的类添加原子操作,但是,这通常做不到,因为无法修改源代码。我们可以扩展这个类,例如BetterVector对Vector进行了扩展,添加一个原子方法,putIfAbsent。

public class BetterVertor<E> extends Vector<E>{
    public synchronized boolean putIfAbsent(E x){
        boolean absent = !contains(x);
        if(absent)
            add(x);
        return absent;
    }
}

  上述示例之所以线程安全,是因为Vector将状态向子类公开,并且规范中定义了同步策略。

客户端加锁机制

  我们可以用第三种方式来在线程安全类中添加功能,扩展类的功能,并不扩展类的本身,将扩展代码放入辅助类中。

public class ListHepler<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;
    }
}

  这个类看起来是线程安全的,毕竟使用了同步方法。然而这并不是线程安全的,问题在于在错误的锁上进行了同步。因为不管list使用哪个锁来保护状态,但肯定不是ListHelper上的锁。意味着putIfAbsent相对于List的其他操作并不是原子的。

  要想使这个方法正确执行,必须是List在实现客户端加锁时使用同一个锁。下面是正确的示例。

public class ListHepler<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;
        }
    }
}

 

组合

  还有一种方式来添加原子操作:组合。

public class ImprovedList<E> {
    private final List<E> list;
    public ImprovedList(List<E> list){
        this.list = list;
    }
    public synchronized boolean putIfAbsent(E x){
        boolean absent = !list.contains(x);
        if(absent)
            list.add(x);
        return absent;
    }
  pubic sunchronized void otherMethod(){
...
}
}

  客户端并不会直接使用list这个对象,因此并不关心list是否是线程安全的,ImprovedList通过自身内置锁增加了一层额外的锁。事实上,我们使用了监视器模式封装了现有的list。只要确保客户端代码不直接使用list就能确保线程安全性。

 

设计线程安全的类--对象的组合