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.同步策略文檔化

在維護線程安全性過程中,文檔是最強大的。一個類是否是線程安全的,需要查閱文檔。
爲類用戶編寫類線程安全性擔保文檔;
爲類的維護者編寫類的同步策略文檔。

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