java并发编程实践学习(3)组合对象

一.设计线程安全的类

设计线程安全类的过程应该包括下面3个基本要素

  • 确定对象状态是由哪些变量构成
  • 确定限制状态变量不受约束
  • 制定一个管理并发访问对象状态的策略
    对象的状态首先要从域说起,如果对象的域都是基本类型,那么这些域就组成了对象的完整状态;如果一个域引用了其他对象,那么他的状态也包含了被引用的对象的域。
    同步策略定义了对象如何协调对其的状态的访问,并且不会违反它的不变约束或是后验条件。他规定如何把不可变性,线程限制和锁结合起来。从而维护线程的安全性,还指明哪些锁保护哪些变量。为了易于维护,同步策略也应该写入文档

1.收集同步需求

收集同步需求是找出复合操作、多个变量遵循原子性的操作等。
对象和变量拥有状态空间:即他们可能处于的状态范围。状态空间越小越容易判断的它们。
尽量使用final类型的域可以简化对对象的可能状态进行分析。
类可以通过不可变约束来判定一种状态是合法的还是非法的。操作的后验条件会指出某种状态转换是非法的,
例如:
线程安全的计数器

@ThreadSafe
public final class Counter{
    @GuardedBy("this") private long value = 0;
    public sychnorized long getValue(){
        return value;
    }
    public sychnorized long increment(){
        if(value == Long.MAX_VALUE){
            throw new IllegalStateEcpetion("counter overflow");
        }
        return ++value;
    }
}

如果操作中可能出现非法状态转换,那么操作必须是原子的。
如果没有强制约束,那么就可以开放一些关于类的封装或序列化条件,以此获得更好的性能。

2.状态依赖的操作

状态依赖的操作是找出操作是否基于先验条件。
有些对象的方法有基于状态的先验条件,例如,你无法从空队列中移除一个项目;所以在移除前需要沿着队列必须处于“非空”状态。
若一个操作存在基于状态的先验条件,则把它称为状态依赖
在单线程中如果先验条件无法满足必然失败,但是在并发程序中,可能由于其他线程的活动使先验条件满足。所以并发程序可以持续等待,直到先验条件为真再继续处理。
java中有等待条件成立的机制-wait和notify与内部锁紧密绑在一起。也可以使用更方便的现有类库(阻塞队列blocking queue,信号量semaphore,以及其他同步工具Synchrnizer

3.状态所有权

状态所有权是指对象被哪些线程所有,哪些线程可以操作对象。从而维护线程安全性。为了分析并维护类,应该将该类同步策略写入文档。

二.实例限制

当一个线程不是线程安全的,你可以通过一种方式来保证所有的访问都正确的被锁访问,这种方式叫做实例限制。
将数据封装到对象的内部,把对数据的访问限制在对象的方法上,更容易确保线程在访问数据时总能获得正确的锁。
使用限制保证线程安全

@ThreadSafe
public class PersonSet{
    @GuardedBy("this")
    private final Set<Person> mySet = new HashSet<Person>();

    public synchronized void addPerson(Person p){
        mySet.add(p);
    }

    public synchronized void addPerson(Person p){
        return mySet.contains(p);
    }
}

PersonSet示范了限制与锁如何协同确保一个类的线程安全性,非线程安全的HashSet管理着PersonSet的状态。但是由于mySet是私有的不会逸出。只可以通过上锁的俩个方法,执行它们都需要获得PersonSet的锁。从而保证了线程安全。
但是Person线程安全性未知。如果他可变。那么从PersonSet中获取Person时还需要额外的同步。可靠的方法是让Person自身线程安全。

java监视器模式

线程限制的直接推论是java监视器模式
监视器可以看做是经过特殊布置的建筑,这个建筑有一个特殊的房间,该房间通常包含一些数据和代码,但是一次只能一个消费者(thread)使用此房间。
监视器模式封装了所有对象的可变状态,并由对象自己的内部锁保护。
私有锁保护状态

public class PrivateLock{
    private final Object myLock = Object();
    @GuardedBy("myLock") Widget widget;

    void someMethod(){
        synchronized(myLock){
            //修改访问Widget的状态
        }
    }
}

三.委托线程安全

Servlet使用AtomicLong 统计请求数

@ThreadSafe
public class CounteingFactorizer implements Servlet{
    private final AtomicLong count = new AtomicLong(0);

    public long getCount() {
        return count.get();
    }

    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        count.incrementAndGet();
        encodeIntResponse(resp,factors);
    }
}

在CountingFactorize中,我们向一个无状态的类中加入了一个AtomicLong类型的属性,所得的组合对象仍然是线程安全的,因为CountingFactorizer的状态就是线程安全类AtomicLong的状态,而且CountingFactorizer并未对counter的状态施加额外的有效性约束,所以CountingFactorizer是线程安全的。
可以说CountingFactorizer将它的线程安全性委托给了AtomicLong:因为AtomicLong是线程安全的,所以CountingFactorizer也是。

1.基于监视器和委托的机动车追踪器

基于监视器的机动车追踪器

@ThreadSafe
public class MonitorVehicleTracker{
    @GuardeBy("this")
    private final Map<String,MutavlePoint> locations;

    public MonitorVehicleTracker(Map<String,MutablePoint> locations){
        this.locations = deepCopy(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 IllegalArgumentException("No such ID" + id);
        }
        loc.x = x;
        loc.y = y;
    }
    private static Map<String,MutablePoint> deepCopy(Map<String,MutablePoint> m){
        Map<String,MultablePoint> result = new HashMap<String,MutablePoint>;
        for(String id : m.keySet()){
            result.put(id,new MutablePoint(m.get(id));
        }
        return Collections.unmodifiableMap(result);
    }
}

@NotThreadSafe
public class MutablePoint{
    public int x,y;

}

基于委托的机动车追踪器 将线程安全委托到CurrentHashMap

@ThreadSafe
public class DelegatingVehicleTracker{
    private final ConcurrentMap<String,Point> locations;
    private final Map<String,Point> unmodifiableMap;

    public DelegatingVehicleTracker(Map<String,Point> points){
        locations = new ConcurrentHashMap<String,Point>(points);
        unmodifiableMap = Collections.unmodifiableMap(locations);
    }
    public Map<String,Point> getLocations(){
        return unmodifiableMap;
    }

    public Point getLocation(String id){
        return locations.get(id);
    }

    public void setLocation(String id,int x,int y){
        if(locations.replace(id,new Point(x,y)) == null)
            throw new IllegalArgumentException("invalid cehicle name:"+id); 
    }
}

2.非状态依赖变量

上面的示例中仅仅委托了一个单一的线程安全的状态变量,我们也可以将线程安全委托到多个隐含的彼此独立的状态变量上
委托线程安全到多个底层的状态变量

public class VisualComponent{
    private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<KeyListener>();
    private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<MouseListener>();

    public void addKeyListener(KeyListener listener){
        keyListeners.add(listener);
    }

    public void addMouseListener(MouseListener listener){
        mouseListeners.add(listener);
    }

    public void removeKeyListener(KeyListener listener){
        keyListeners.remove(listener);
    }

    public void removeMouseListener(MouseListener listener){
        mouseListeners.remove(listener);
    }
}

在VisualComponet中,不但每个list是线程安全的,而且不会存在哪个不变约束会增加与另一个状态间的耦合,所以VisualComponent可以将它的线程安全委托到mouseListeners和keyListenrers上。

3.当委托无法胜任时

在下面的的例子中,不变约束不像上面那么简单,它有一个额外的约束限制: 第一个数小于或等于第二个。
NumberRange没有完整的保护它的不变约束(不要这样做)

public class NumberRange{
    //不变约束; lower<=upper
    private void setLower(int i){
        //警告--不安全的“检查再运行”
        if(i > upper.get())
            throw new IllegalArgumentException("can't set lower to" + i + ">upper";
        lower.set(i);
    }

    private void setLowerUpper(int i){
        //警告--不安全的“检查再运行”
        if(i < lower.get())
            throw new IllegalArgumentException("can't set Upper to" + i + "<lower";
        upper.set(i);
    }

    public boolean isInRange(int i){
        return (i>= lower.get() && i<=upper.get());
    }
}

Number 不是线程安全的,setLower和setUpper都是“检查在运行”,但是它们没有适当的加锁以保证原子性。当俩个线程分别调用setLower(5)和setUpper(4),在可能情况下它们都能满足检查使修改生效但是却是无效状态。
因为状态变量lower和upper不是彼此独立的不能进行简单的委托。可以加锁来解决这个问题。

如果一个类由多个彼此独立的线程安全的状态变量组成,并且类的操作不包含任何无效的状态转换时,可以委托线程安全给状态变量。

四.向已有线程安全类中添加功能

有时一个线程安全类不能支持我们需要的全部操作,这时我们需要在不破坏其线程安全性的是前提下向它添加一个新的操作。

1.客户端加锁

这种策略是拓展功能而不是拓展类本身。
使用客户端加锁实现“缺少即加入”

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

客户端加锁必须保证客户端代码与对象x保护自己状态时使用的锁是同一个锁。

2.组合

向已有的类中添加一个原子操作还有更健壮的操作:组合
使用组合实现“缺少即加入”

@ThreadSafe
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 contains = list.contains(x);
        if(contains)
            list.add(x);
        return !contains;   
    }

    public synchronized void clear(){
        list.clear();
    }
    //...simlarly delegate other List methods
}

通过使用内部锁,ImprovedList引入了新的锁层。它不关心底层的List是否线程安全。
ImprovedList有自己兼容的锁可以提供线程安全性,虽然额外一层的同步可能带来微弱的性能损失,但是只要底层List有唯一外部引用就能保证线程安全性。

5.同步策略文档化

在维护线程安全性过程中,文档是最强大的。一个类是否是线程安全的,需要查阅文档。
为类用户编写类线程安全性担保文档;
为类的维护者编写类的同步策略文档。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章