1. 程式人生 > >從零開始學多執行緒之組合物件(三)

從零開始學多執行緒之組合物件(三)

前文回顧

通過博主之前釋出的兩篇部落格從零開始學多執行緒之執行緒安全(一)從零開始學多執行緒之共享物件(二)講解的知識點,我們現在已經可以構建執行緒安全的類了,本篇將為您介紹構建類的模式,這些模式讓類更容易成為執行緒安全的,並且不會讓程式意外破壞這些類的執行緒安全性.

本篇部落格將要講解的知識點

  1. 構建執行緒安全類要關注那些因素.
  2. 使用例項限制+鎖的模式,使非執行緒安全的物件,可以被併發的訪問。
  3. 擴充套件一個執行緒安全的類的四種方式

構建執行緒安全的類

我們已經知道多執行緒操縱的類必須是執行緒安全的,否則會引發種種問題,那麼如何設計執行緒安全的類呢?我們可以從以下三個方面考慮:

  1. 確定物件狀態是由哪些變數構成的
    ;
  2. 確定限制狀態變數的不變約束;
  3. 制定一個管理併發訪問物件狀態的策略

當我們想要建立一個執行緒安全的類的時候,首先要關注的就是這個類的成員變數是否會被釋出,如果被髮布,那麼就要根據物件的可變性(可變物件、不可變物件、高效不可變物件)去決定如何釋出這個物件(如果不明白安全釋出的概念,請移駕從零開始學多執行緒之共享物件(二))

然後再看狀態是否依靠外部的引用例項化:如果一個物件的域引用了其他物件,那麼它的狀態也同時包含了被引用物件的域.

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監視器模式)的方式,安全的訪問它們.

我們還學會了如何擴充套件一個執行緒安全的的類:擴充套件有四法,組合是最佳.

下一篇部落格,我會為介紹幾種常用的執行緒安全容器同步工具.來構建執行緒安全的類.

好了本篇部落格就分享到這裡,我們下篇再見.