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

來源:互聯網
上載者:User

標籤:大於   迭代   精確   根據   for   ica   分段   建立   bsp   

一、背景

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

通常有兩種選擇:

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

2、使用java.util.concurrent包下提供的並發容器。比如常見的ConcurrentHashMap、CopyOnWriteArrayList等。

第一種選擇的優點是上手快,簡單直接,易於調試,如果不考慮效能的話,幾乎沒有任何使用情境的限制,可以保證資料操作的強一致性;那麼它的缺點也是很明顯的,由於每次對容器的操作都鎖住了整個容器,如果對容器進行高並發的操作,將導致操作效能急劇下降。

第二種選擇的優點是concurrent包下的並發容器通常都做了效能上的高度最佳化,能保障高並發情境下的操作效能;但缺點是這些容器的背後實現原理相對複雜,而且對使用情境有一定限制,一般只能保證資料操作的弱一致性。

本文將重點介紹並發容器背後的典型設計思路與實現原理,讀者瞭解了這些實現思路後,也可以更好的理解並發容器的使用情境的限制。

二、ConcurrentHashMap的設計理念

關於ConcurrentHashMap的實現原理,在JDK1.8與JDK1.8之前有不同的實現,關於它們具體的實現細節網上已經有很多優秀的文章進行介紹,比如:

1、《JDK1.7 ConcurrentHashMap原理分析》

2、《JDK1.8 ConcurrentHashMap原理分析》

3、 《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)。

三、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可以保證的是,讀操作可以讀取到最近一次更新完成的結果。

寫時複製技術因為每次修改都需要完整拷貝一次底層數組,所以有額外的效能開銷,但是特別適用於讀多寫少的資料訪問情境。

四、總結

1、ConcurrentHashMap和CopyOnWriteArrayList都是無鎖化的讀取,所以讀操作發生時無法確保目前所有其他線程的寫操作已經完成,不可用於要求資料強一致性的情境。

2、ConcurrentHashMap和CopyOnWriteArrayList都可以保證讀取時可以感知到已經完成的寫操作。

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

4、ConcurrentHashMap使用鎖分段技術,縮小鎖的範圍,提高寫的並發量。CopyOnWriteArrayList使用寫時複製技術,保證並發寫入資料時,不會對已經開啟的讀操作造成幹擾。

5、ConcurrentHashMap適用於高並發下對資料訪問沒有強一致性需求的情境。CopyOnWriteArrayList適用於高並發下能夠容忍唯讀取到曆史快照資料,且讀多寫少的情境。

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.