併發編程學習筆記之組合對象(三)

換了個markdown的編輯器,感受挺方便的,可是手機端的格式顯示不正確,若是讀者是手機端用戶,點擊右上角做者主頁查看,就能夠了html

前文回顧

經過博主以前發佈的兩篇博客從零開始學多線程之線程安全(一)從零開始學多線程之共享對象(二)講解的知識點,咱們如今已經能夠構建線程安全的類了,本篇將爲您介紹構建類的模式,這些模式讓類更容易成爲線程安全的,而且不會讓程序意外破壞這些類的線程安全性.java

本篇博客將要講解的知識點

  1. 構建線程安全類要關注那些因素.
  2. 使用實例限制+鎖的模式,使非線程安全的對象,能夠被併發的訪問。
  3. 擴展一個線程安全的類的四種方式

構建線程安全的類

咱們已經知道多線程操縱的類必須是線程安全的,不然會引起種種問題,那麼如何設計線程安全的類呢?咱們能夠從如下三個方面考慮:設計模式

  1. 肯定對象狀態是由哪些變量構成的;
  2. 肯定限制狀態變量的不變約束;
  3. 制定一個管理併發訪問對象狀態的策略

當咱們想要建立一個線程安全的類的時候,首先要關注的就是這個類的成員變量是否會被發佈,若是被髮布,那麼就要根據對象的可變性(可變對象、不可變對象、高效不可變對象)去決定如何發佈這個對象(若是不明白安全發佈的概念,請移駕從零開始學多線程之共享對象(二))安全

而後再看狀態是否依靠外部的引用實例化:若是一個對象的域引用了其餘對象,那麼它的狀態也同時包含了被引用對象的域.markdown

public class Domain {
  private Object obj;

  public Domain(Object obj) {
      this.obj = obj;
  }
}

這時候就要保證傳入的obj對象的線程安全性.不然obj對象在外部被改變,除修改線程之外的線程,不必定能感知到對象已經被改變,就會出現過時數據的問題.多線程

咱們應該儘可能使用final修飾的域,這樣能夠簡化咱們對對象的可能狀態進行分析(起碼保證只能指向一塊內存地址空間).併發

而後咱們再看類的狀態變量是否涉及不變約束,並要保護類的不變約束編輯器

public class Minitor {
    private long value = 0;

    public synchronized  long getValue(){
        return value;
    }

    public synchronized long increment(){
        if(value == Long.MAX_VALUE){
            throw new IllegalStateException(" counter overflow");
        }
        return ++value;
    }
}

咱們經過封裝使狀態value沒有被髮布出去,這樣就杜絕了客戶端代碼將狀態置於非法的情況,保護了不變約束if(value == Long.MAX_VALUE).工具

維護類的線程安全性意味着要確保在併發訪問的狀況下,保護它的不變約束;這須要對其狀態進行判斷.性能

increment()方法,是讓value++進行一次自增操做,若是value的當前值是17,那麼下一個合法值是18,若是下一狀態源於當前狀態,那麼操做必須是原子操做.

這裏涉及到線程安全的可見性與原子性問題,若是您對此有疑問請移駕從零開始學多線程之線程安全(一)

實例限制

一個非線程安全的對象,經過實例限制+鎖,可讓咱們安全的訪問它.

實例限制:把非線程安全的對象包裝到自定義的對象中,經過自定義的對象去訪問非線程安全的對象.

public class ProxySet {
    private Set<String> set = new HashSet<>();

    public synchronized void add(String value){
        set.add(value);
    }

    public synchronized  boolean contains(String value){
        return set.contains(value);
    }
}

HashSet是非線程安全的,咱們把它包裝進自定義的ProxySet類,只能經過ProxySet加鎖的方法操做集合,這樣HashSet又是線程安全的了.

若是咱們把訪問修飾符改成public的,那麼這個集合仍是線程安全的嗎?

public Set<String> set = new HashSet<>();

這時候其它線程就能夠獲取到這個set集合調用add(),那麼Proxyset的鎖就沒法起到做用了.因此他又是非線程安全的了.因此咱們必定不能讓實例限制的對象逸出.

將數據封裝在對象內部,把對數據的訪問限制在對象的方法上,更易確保線程在訪問數據時總能得到正確的鎖

實例限制使用的是監視器模式,監視器模式的對象封裝了全部的可變狀態,並由本身的內部鎖保護.(完成多線程的博客後,博主就會更新關於設計模式的博客).

擴展一個線程安全的類

咱們使用Java類庫提供的方法能夠解決咱們的大部分問題,可是有時候咱們也須要擴展java提供的類沒有的方法.

如今假設咱們要給同步的list集合,擴展一個缺乏即加入的方法(必須保證這個方法是線程安全的,不然可能某一時刻會出現加入兩個同樣的值).

咱們有四種方法能夠實現這個功能:

  1. 修改原始的類
  2. 擴展這個類(繼承)
  3. 擴展功能而,不是擴展類自己(客戶端加鎖,在調用這個對象的地方,使用對象的鎖確保線程安全)
  4. 組合

咱們一個一個來分析以上方法的利弊.

1.修改原始的類:

優勢: 最安全的方法,全部實現類同步策略的代碼仍然包含在要給源代碼文件中,所以便於理解與維護.

缺點:可能沒法訪問源代碼或沒有修改的自由.

2.擴展這個類:

優勢:方法至關簡單直觀.

缺點:並不是全部類都給子類暴露了足夠多的狀態,以支持這種方案,還有就是同步策略的
實現會被分佈到多個獨立維護的源代碼文件中,因此擴展一個類比直接在類中加入代碼更脆弱.若是底層的類選擇了
不一樣的鎖保護它的狀態變量,從而會改變它的同步策略,子類就在不知不覺中被破壞,
由於他不能再用正確的鎖控制對基類狀態的併發訪問.

3.擴展功能而,不是擴展類自己:

public class Lock {
    public List<String> list = Collections.synchronizedList(new ArrayList<String>());

    public  synchronized boolean putIfAbsent(String value){
        boolean absent = !list.contains(value);
        if(!absent){
            list.add(value);
        }
        return absent;
    }
}

這個方法是錯的.使用synchronized關鍵字雖然同步了缺乏即加入方法, 並且使用list也是線程安全的,可是他們用的不是同一個鎖,list因爲pulic修飾符,任意的線程均可以調用它.那麼在某一時刻,知足if(!absent)不變約束的同時準備add()這個對象的時候,已經有另外一個線程經過lock.list.add()過這個對象了,因此仍是會出現add()兩個相同對象的狀況.

正確的代碼,要確保他們使用的是同一個鎖:

public class Lock {
    public List<String> list = Collections.synchronizedList(new ArrayList<String>());

    public   boolean putIfAbsent(String value){
        synchronized(list){
        boolean absent = !list.contains(value);
        if(!absent){
            list.add(value);
        }
            return absent;
        }
    }
}

如今都使用的是list對象的鎖,因此也就不會出現以前的狀況了.

這種方式叫客戶端加鎖.

優勢: 比較簡單.

缺點: 若是說爲了添加另外一個原子操做而去擴展一個類容易出問題,是由於它將加鎖的代碼分佈到對象繼承體系中的多個類中.然而客戶端加鎖實際上是更加脆弱的,由於他必須將類C中的加鎖代碼(locking code)置入與C徹底無關的類中.在那些不關注鎖策略的類中使用客戶端加鎖時,必定要當心

客戶端加鎖與擴展類有不少共同之處--所得類的行爲與基類的實現之間都存在耦合.正如擴展會破壞封裝性同樣,客戶端加鎖會破壞同步策略的封裝性.

  1. 組合對象:
public class ImprovedList<T> implements List<T> {
    private final List<T> list;

    public ImprovedList(List<T> list) {
        this.list = list;
    }

    public synchronized boolean putIfAbsent(Object obj){
        boolean absent = list.contains(obj);
        if(absent){
            list.add((T) obj);
        }
        return absent;
    }
}

經過ImprovedList對象來操做傳進來的list對象,用的都是Improved的鎖.即便傳進來的list不是線程安全的,ImprovedList也能保證線程安全.

優勢:相比以前的方法,這種方式提供了更健壯的代碼.

缺點:額外的同步帶來一些微弱的性能損失.

總結

本篇博客咱們講解了,要設計線程安全的類要從三個方面考慮:

  1. 肯定對象狀態是由哪些變量構成的;
  2. 肯定限制狀態變量的不變約束;
  3. 制定一個管理併發訪問對象狀態的策略

對於非線程安全的對象,咱們能夠考慮使用鎖+實例限制(Java監視器模式)的方式,安全的訪問它們.

咱們還學會了如何擴展一個線程安全的的類:擴展有四法,組合是最佳.

下一篇博客,我會爲介紹幾種經常使用的線程安全容器同步工具.來構建線程安全的類.

好了本篇博客就分享到這裏,咱們下篇再見.

相關文章
相關標籤/搜索