別再問我ConcurrentHashMap了

2022-03-26 13:12:42 字數 3566 閱讀 4791

以下concurrenthashmap以jdk8中為例進行分析,concurrenthashmap是乙個執行緒安全、基於陣列+鍊錶(或者紅黑樹)的kv容器,主要特性如下:

concurrenthashmap預設陣列長度16,map最大容量為maximum_capacity = 1 << 30。建立concurrenthashmap並不是涉及陣列的初始化,陣列初始化是在第一次put資料才進行的。(注意:jdk1.8中捨棄了之前的分段鎖技術,改用cas+synchronized機制)

concurrenthashmap中乙個重要的類就是node,該類儲存鍵值對,所有插入concurrenthashmap的資料都包裝在這裡面。它與hashmap中的定義很相似,但是有一些差別是concurrenthashmap的value和next屬性都是volatile的(保證了get資料時直接返回即可,volatile保證了更新的可見性),且不允許呼叫setvalue方法直接改變node的value域,增加了find方法輔助map.get()方法,可在get方法返回的結果中更改對應的value值。

1

static

class nodeimplements map.entry

concurrenthashmap定義了三個原子操作,用於對陣列指定位置的節點進行操作。正是這些原子操作保證了concurrenthashmap的執行緒安全。

1

//獲得在i位置上的node節點

2static

final

nodetabat(node tab, int

i) 5//

利用cas演算法設定i位置上的node節點。之所以能實現併發是因為他指定了原來這個節點的值是多少 6//

在cas演算法中,會比較記憶體中的值與你指定的這個值是否相等,如果相等才接受你的修改,否則拒絕你的修改 7//

因此當前執行緒中的值並不是最新的值,這種修改可能會覆蓋掉其他執行緒的修改結果

8static

final

boolean castabat(node tab, int

i,

9 nodec, nodev)

12//

利用volatile方法設定節點位置的值

13static

final

void settabat(node tab, int i, nodev)

紅黑樹節點比較既然使用到了紅黑樹,這就涉及到節點的大小比較問題(節點資料報含key、value資訊)。進行節點的大小比較時,首先是比較節點的hash值,注意hash值不是hashcode,因為hash值是物件hashcode與自己無符號右移16位進行異或後的值。如果節點的hash值相等,判斷節點的key物件是否實現了comparable介面,實現的話就是用comparable邏輯比較節點之間的大小。如果key物件未實現comparable介面,則呼叫tiebreakorder方法進行判斷:

1

//dir = tiebreakorder(k, pk); k/pk,帶比較兩個節點,命名還是挺有意思的

2static

inttiebreakorder(object a, object b)

這裡呼叫了system.identityhashcode,將由預設方法hashcode()返回,如果物件的hashcode()被重寫,則system.identityhashcode和hashcode()的返回值就不一樣了。put原始碼

1

final v putval(k key, v value, boolean

onlyifabsent)

16else

if ((fh = f.hash) ==moved)

17//

在rehash擴容,幫助擴容,擴容完成之後才能繼續進行put操作

18 tab =helptransfer(tab, f);

19else

35 nodepred =e;

36if ((e = e.next) == null

) 41}42

}43else

if (f instanceof treebin) 52}

53}54}

55if (bincount != 0) 62}

63}64//

增加統計值,可能觸發rehash擴容

65 addcount(1l, bincount);

66return

null;67

}6869private

final

void addcount(long x, int

check)

86if (check <= 1)

87return

;88 s =sumcount();89}

90if (check >= 0)

104else

if (u.compareandswapint(this

, sizectl, sc,

105 (rs << resize_stamp_shift) + 2))

106 transfer(tab, null

);107 s =sumcount();

108}

109}

110 }

get方法比較簡單,給定乙個key來確定value的時候,必須滿足兩個條件hash值相同同時 key相同(equals) ,對於節點可能在鍊錶或樹上的情況,需要分別去查詢。

get時一般是不加鎖(node節點中value資料型別是volatile的,保證了記憶體可見性)。如果slot元素為鍊錶,直接讀取返回即可;如果slot元素為紅黑樹,並且此時該樹在進行再平衡或者節點刪除操作,讀取操作會按照樹節點的next指標進行讀取,也是不加鎖的;如果該樹並沒有進行平衡或者節點刪除操作,那麼會用cas加讀鎖,防止讀取過程中其他執行緒該樹進行更新操作,破壞「讀檢視」。

remove流程就是根據key找到對應節點,將該節點從鍊錶(更改節點前後關係)或者紅黑樹移除的過程,注意,從紅黑樹中刪除元素後,不會將紅黑樹轉換為列表的,只能在put元素時列表可能有轉換紅黑樹操作,不會有反向操作。

注意:hashmap有自動rehash擴容機制,但是當元素remove之後並沒有自動縮容機制,如果陣列經過多次擴容變得很大,並且當前元素較少,請將這些元素轉移到乙個新的hashmap中。

rehash時是成倍擴容(老table和新tablenew),對於table中i位置的所有元素,擴容後會被分配到i和i+table.length這兩個位置中。rehash主要的流程transfer方法中,具體不再展開。

推薦閱讀:

拜託,面試別再問我跳表了!

跳表是乙個隨機化的資料結構,實質就是一種可以進行二分查詢的有序鍊錶。跳表在原有的有序鍊錶上面增加了多級索引,通過索引來實現快速查詢。跳表不僅能提高搜尋效能,同時也可以提高插入和刪除操作的效能。考慮乙個有序鍊錶,我們要查詢3 7 17這幾個元素,我們只能從頭開始遍歷鍊錶,直到查詢到元素為止。上述這個鍊...

拜託,面試別再問我TopK了!!!

除非校招,我在面試過程中從不問topk這個問題,預設大家都知道。將n個數排序之後,取出最大的k個 即為所得。sort arr,1,n return arr 1,k o n lg n 明明只需要topk,卻將全域性都排序了,這也是這個方法複雜度非常高的原因。那能不能不全域性排序,而只區域性排序呢?這就...

拜託,面試別再問我計數排序了!!!

radix sort 還有計數排序 counting sort 今天,1分鐘,通過幾幅圖,爭取讓大家搞懂計數排序。空間大小為o max min 用來儲存所有元素出現次數 計數 掃瞄待排序資料 arr n 使用計數陣列 counting max min 對每乙個 arr n 現的元素進行計數 掃瞄計數...