Java進階知識點6:併發容器背後的設計理念 - 鎖分段、寫時複製和弱一致性

1、背景

容器是Java編程中使用頻率很高的組件,但Java默認提供的基本容器(ArrayList,HashMap等)均不是線程安全的。當容器和多線程併發編程相遇時,程序員又該何去何從呢?html

一般有兩種選擇:java

一、使用synchronized關鍵字,將對容器的操做有序錯開,確保同一時刻對同一個容器只存在一個操做。Vector,HashTable等封裝後的容器本質也是這種解決思路,只不過synchronized關鍵字不須要咱們來書寫而已。程序員

二、使用java.util.concurrent包下提供的併發容器。好比常見的ConcurrentHashMap、CopyOnWriteArrayList等。編程

第一種選擇的優勢是上手快,簡單直接,易於調試,若是不考慮性能的話,幾乎沒有任何使用場景的限制,能夠保證數據操做的強一致性;那麼它的缺點也是很明顯的,因爲每次對容器的操做都鎖住了整個容器,若是對容器進行高併發的操做,將致使操做性能急劇降低。數組

第二種選擇的優勢是concurrent包下的併發容器一般都作了性能上的高度優化,能保障高併發場景下的操做性能;但缺點是這些容器的背後實現原理相對複雜,並且對使用場景有必定限制,通常只能保證數據操做的弱一致性。安全

本文將重點介紹併發容器背後的典型設計思路與實現原理,讀者瞭解了這些實現思路後,也能夠更好的理解併發容器的使用場景的限制。多線程

2、ConcurrentHashMap的設計理念

關於ConcurrentHashMap的實現原理,在JDK1.8與JDK1.8以前有不一樣的實現,關於它們具體的實現細節網上已經有不少優秀的文章進行介紹,好比:併發

一、《JDK1.7 ConcurrentHashMap原理分析》app

二、《JDK1.8 ConcurrentHashMap原理分析》jvm

三、 《ConcurrentHashMap在JDK1.7與JDK1.8中的對比》

此處便不在贅述了。

本文重點用簡潔易懂的語言帶領讀者快速掌握ConcurrentHashMap在JDK1.8中高併發實現的原理。

2.1 普通HashMap實現原理回顧

首先咱們簡單回顧一下普通HashMap的實現原理。

 

 

 如上圖所示,咱們將Map中儲存的每個Entry抽象爲一個Node。Node根據其Key值Hash取餘後,映射到Table(一個Node數組)的某一個槽位上進行儲存。若是出現Hash衝突(即兩個Node的Key值Hash取餘結果相同),則以鏈表的形式在出現衝突的Table槽位上繼續追加Node。若是某一個槽位以鏈表的形式儲存了過多的Node(8個以上),則將鏈表轉換爲紅黑樹儲存,避免查詢Node時對長鏈表的遍歷,以下降查詢Node的時間複雜度。當Map中容納的Node總數大於Table長度乘以加載因子factor(默認0.75)時,Map會將Table成倍擴容,以減小Hash衝突的機率。

2.2 ConcurrentHashMap併發優化思路一:儘可能減小鎖的範圍(鎖分段)

傳統的HashTable之因此併發性能不好,緣由在於鎖的範圍過大,更新任何一個數據,都要將全Map鎖住。

其實中HashMap的實現原理不難看出,HashMap自己自然就呈現出邊界清晰的分段儲存特性,即每個Table中的一個槽位,便可認爲是一個儲存段。那麼,若是咱們將鎖的精度精確到每個儲存段,就能夠實現更新每個數據,只會對與該數據相關的局部數據段加鎖。而每一個儲存段的頭結點,便可做爲加鎖對象。

JDK1.8中的核心源碼以下:

Node<K,V> f;
f = tabAt(tab, i = (n - 1) & hash); //取出Tab指定槽中的頭結點
synchronized (f) { //對這個頭結點加鎖 
//... ...
}

若是某個槽位中尚不存在任何頭結點(即頭結點爲null),此時咱們不能對null進行加鎖,又如何規避該槽位首次插入Node時可能遭遇的併發衝突呢?

可使用CAS(Compare And Swap(Set))進行Node的首次插入。CAS的核心原理是更新某個數據前,檢查該數據的值是否仍是以前獲取獲得的舊值,若是是則說明該值尚未被其餘線程修改,能夠直接修改成新值,不然則說明該值已經被其餘線程修改了,則設置失敗。檢查舊值是否被修改與設置新值這兩步操做由CPU提供的單指令直接完成,保證原子性。

使用CAS技術加上CAS失敗後的不斷重試,便可實現無鎖化更新數據。畢竟CAS失敗的機率很低,不斷重試也不會佔用過多CPU。(樂觀鎖與自旋鎖的理念)

JDK1.8中的核心源碼以下:

for (Node<K, V>[] tab = table; ; ) {
     if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        if (casTabAt(tab, i, null,
                new Node<K, V>(hash, key, value, null)))
            break; //CAS失敗則跳出循環,開始下一次循環,從新讀取頭結點
    }
}

2.3 ConcurrentHaspMap併發優化思路二:只對更新加鎖,讀不加鎖 (弱一致性)

ConcurrentHashMap的讀操做都是不加鎖的。能夠保證的是,讀取某一個指定key的值時能夠讀取到最近一次更新完成的結果。更標準的說法是,上一次對keyA的更新結果happens-before後續對keyA的讀取操做。

注:happens-before是jvm用來定義兩個action之間(acitonA和actionB)的偏序關係,從而明確在CPU容許重排序的狀況下,actionA發生的結果是必定要對後續發生的actionB可見的。

因爲讀操做不加鎖,讀操做可能會與其餘線程的寫操做重疊,ConcurrentHashMap可能會讀取到其餘線程寫操做的中間狀態。好比putAll在執行過程當中有併發的get操做,那麼get操做可能只會讀取到插入的部分數據,同時併發的size操做的返回結果也是不許確的,只可用於估算類業務,不可用於精準的控制流程判斷。再好比使用迭代器遍歷Map時,另一個線程正在刪除Map,那麼在讀取過程當中碰巧尚未被刪除的數據會被讀取到,而已經被刪除的數據不會被讀取到(不會拋出ConcurrentModificationException)。

3、CopyOnWriteArrayList的設計理念

3.1 CopyOnWriteArrayList併發優化思路:寫時複製與弱一致性

所謂寫時複製,即任何要改變CopyOnWriteArrayList的操做(add、set等),其內部實現都是深拷貝一份CopyOnWriteArrayList的底層數組,而後在深拷貝的副本上進行數據的修改。修改完成後,再用新的副本與替換原來的CopyOnWriteArrayList底層數組。

JDK1.8中的核心代碼以下:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1); //深拷貝底層數組
        newElements[len] = e; //在副本上進行修改
        setArray(newElements); //修改完成後用副本替換底層數組
        return true;
    } finally {
        lock.unlock();
    }
}

寫時複製的好處是,任何的讀操做都不用加鎖,並且保證讀取到的是讀那一刻List完整的快照數據。好比當CopyOnWriteArrayList的迭代器建立後,不管List自己如何變化,迭代器能感知到的都是它在被建立那一刻時List的狀態,任何其餘線程對List的改變,對本迭代器都不可見。不會出現ConcurrentHashMap的迭代器可能讀取到其餘線程修改過程當中容器的中間狀態的狀況。因爲CopyOnWriteArrayList讀操做沒法感知最新正在變化的數據,因此CopyOnWriteArrayList也是弱一致性的。

CopyOnWriteArrayList能夠保證的是,讀操做能夠讀取到最近一次更新完成的結果。

寫時複製技術由於每次修改都須要完整拷貝一次底層數組,因此有額外的性能開銷,可是特別適用於讀多寫少的數據訪問場景。

4、總結

一、ConcurrentHashMap和CopyOnWriteArrayList都是無鎖化的讀取,因此讀操做發生時沒法確保目前全部其餘線程的寫操做已經完成,不可用於要求數據強一致性的場景。

二、ConcurrentHashMap和CopyOnWriteArrayList均可以保證讀取時能夠感知到已經完成的寫操做。

三、ConcurrentHashMap讀操做可能會感知到同一時刻其餘線程對容器寫操做的中間狀態。CopyOnWriteArrayList永遠只會讀取到容器在讀取時刻的快照狀態。

四、ConcurrentHashMap使用鎖分段技術,縮小鎖的範圍,提升寫的併發量。CopyOnWriteArrayList使用寫時複製技術,保證併發寫入數據時,不會對已經開啓的讀操做形成干擾。

五、ConcurrentHashMap適用於高併發下對數據訪問沒有強一致性需求的場景。CopyOnWriteArrayList適用於高併發下可以容忍只讀取到歷史快照數據,且讀多寫少的場景。

相關文章
相關標籤/搜索