JAVA併發編程(七):併發容器(ConcurrentHashMap)

volatile_logo

我們上節講了HashMap,實際上HashMap並不是線程安全的,在併發插入元素的時候有可能出現環形鏈表,讓下一次讀操作出現死循環。解決的辦法就是使用線程安全的容器,除了Collections提供的synchronizedMap同步容器外,實際上我們還可以選擇性能更好的juc提供的同步容器。

一、分段鎖Segment概述

分段鎖Segment是ConcurrentHashMap很重要的一個概念。

Segment本身就相當於一個HashMap對象。

同HashMap一樣,Segment包含一個HashEntry數組,數組中的每一個HashEntry既是一個鍵值對,也是一個鏈表的頭節點。

像這樣的Segment對象,在ConcurrentHashMap集合中有2的N次方個,共同保存在一個名爲segments的數組當中。
因此整個ConcurrentHashMap的結構如下:

ConHashMap_1

可以說,ConcurrentHashMap是一個二級哈希表。在一個總的哈希表下面,有若干個子哈希表。
這樣的二級結構,和數據庫的水平拆分有些相似。每一個Segment就好比一個高度自治的自治區。讀寫高度自治,Segment之間互不影響。

這種結構下的ConcurrentHashMap有以下特點:

  • 不同Segment的寫入是可以併發執行的。
  • 同一Segment的寫和讀是可以併發執行的。
  • 對同一Segment的併發寫入會被阻塞。

由此可見,ConcurrentHashMap當中每個Segment各自持有一把鎖。在保證線程安全的同時降低了鎖的粒度,讓併發操作效率更高。

二、ConcurrentHashMap讀寫概述

###Get方法:

1.爲輸入的Key做Hash運算,得到hash值。
2.通過hash值,定位到對應的Segment對象
3.再次通過hash值,定位到Segment當中數組的具體位置。

###Put方法:

1.爲輸入的Key做Hash運算,得到hash值。
2.通過hash值,定位到對應的Segment對象
3.獲取可重入鎖
4.再次通過hash值,定位到Segment當中數組的具體位置。
5.插入或覆蓋HashEntry對象。
6.釋放鎖。

從以上步驟可以看出,ConcurrentHashMap在讀寫時都需要兩次定位(Hash)操作。

三、ConcurrentHashMap的size()方法

Size方法的目的是統計ConcurrentHashMap的總元素數量, 自然需要把各個Segment內部的元素數量彙總起來。

但是,如果在統計Segment元素數量的過程中,已統計過的Segment瞬間插入新的元素,這時候該怎麼辦呢?

ConcurrentHashMap的Size方法是一個嵌套循環,大體邏輯如下:

1.遍歷所有的Segment。
2.把Segment的元素數量累加起來。
3.把Segment的修改次數累加起來。
4.判斷所有Segment的總修改次數是否大於上一次的總修改次數。如果大於,說明統計過程中有修改,重新統計,嘗試次數+1;如果不是。說明沒有修改,統計結束。
5.如果嘗試次數超過閾值,則對每一個Segment加鎖,再重新統計。
6.再次判斷所有Segment的總修改次數是否大於上一次的總修改次數。由於已經加鎖,次數一定和上次相等。
7.釋放鎖,統計結束。

這種思想和樂觀鎖悲觀鎖的思想如出一轍。

爲了儘量不鎖住所有Segment,首先樂觀地假設Size過程中不會有修改。當嘗試一定次數,才無奈轉爲悲觀鎖,鎖住所有Segment保證強一致性。

參考文章


本文作者: catalinaLi
本文鏈接: http://catalinali.top/2018/knowConHashMap/

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