1. 程式人生 > 實用技巧 >ConcurrentHashMap原理分析(一)

ConcurrentHashMap原理分析(一)

ConcurrentHashMap從JDK1.5開始隨java.util.concurrent包一起引入JDK中,主要為了解決HashMap執行緒不安全和Hashtable效率

不高的問題。眾所周知,HashMap在多執行緒程式設計中是執行緒不安全的,而Hashtable由於使用了synchronized修飾方法而導致執行效率不高;

因此,在concurrent包中,實現了ConcurrentHashMap以使在多執行緒程式設計中可以使用一個高效能的執行緒安全HashMap方案。

1、初始化——ConcurrentHashMap的構造方法

(1)無參構造方法

預設的容量大小為16,區域情況按照(2)所示進行增加

(2)有參構造方法(傳入自定義的初始容量大小)

public ConcurrentHashMap(int initialCapacity) {
//這裡進行了再次判斷,保證初始容量大小為正數,否則丟擲異常
if (initialCapacity < 0)
throw new IllegalArgumentException();
//這裡時jdk1.8版本,當容量設定大於(MAXIMUM_CAPACITY >>> 1)(大小為1073741824)時,就恆定為MAXIMUM_CAPACITY
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?

MAXIMUM_CAPACITY :
//其餘情況走這一步,初始化大小是(執定值+ 執定值/2 +1) 的最大最近的2的冪次方
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}

初始化完成後,
sizeCtl的大小就定了,為map的容量,這個變數的取值如下:

sizeCtl為0 代表陣列沒有初始化
sizeCtl為正數 陣列已經初始化,記錄的是陣列的擴容閾值,如果沒有初始化,記錄的是陣列初始容量


sizeCtl 為-1 代表陣列正在初始化
sizeCtl 為負數,並且不是-1,表示陣列正在擴容

2、Map的put操作

2.1 邏輯圖

2.2、原始碼(解析是自己寫的,有錯望各位指出)

/** Implementation for put and putIfAbsent */
//引數:key:value,onlyIfAbsent預設為false,表示值一樣時不覆蓋
final V putVal(K key, V value, boolean onlyIfAbsent) {
//增強判斷,正常情況下不會走這一步
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());//釋放獲取到的cpu
//正常情況下從這裡開始
int binCount = 0;
//剛定義出來時table為null
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//第一步,map初始化,初始化完成返回設定好容量的table,sizeCtl,完成之後回到迴圈中
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//拿出次key hash位置的value,如果為空則去新增

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//新增的時候是個cas操作
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//此判斷成立表示陣列正在擴容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
//再次判斷是因為 此位置的value有可能變為紅黑樹,鎖就沒用了

if (tabAt(tab, i) == f) {
//如果成立說明是連結串列

if (fh >= 0) {
binCount = 1;
//f是從map中取出來的節點,賦值給e
for (Node<K,V> e = f;; ++binCount) {
K ek;//獲取到鍵
//判斷,從map取出來的位置的hash與node的hash相同
if (e.hash == hash &&
//先將鍵賦值給ek,判斷是否與傳入的key相同
((ek = e.key) == key ||
//為空的話判斷傳入的key是否與map中取出的節點相同(key和value均相同)
(ek != null && key.equals(ek)))) {
//如果相同就將取出來的節點的值賦給oldValue
oldVal = e.val;
if (!onlyIfAbsent)//一般是true
//將值放入取出來的節點
e.val = value;
break;
}
//構造連結串列新增值
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//紅黑樹
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//計數
addCount(1L, binCount);
return null;
}
3、ConcurrenthashMap解決執行緒安全問題

幾乎都是通過cas操作,成功則執行邏輯,cas失敗則自旋等待,直到操作完才會返回結果
參考下面這段原始碼的邏輯
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//sizeCtl小於0,代表 陣列正在初始化或者正在擴容
//此處可以無鎖的防止併發,遷移和擴容的時候,進入到這裡的執行緒都會釋放cpu資源
if ((sc = sizeCtl) < 0)
Thread.yield(); // 釋放cpu
//這裡開始是初始化
//如果已經有執行緒進入,SIZECTL和sc是不一樣的,cas不會成功,會一直自旋等待,直到自己滿足下面這個條件
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
          //雙重判斷是為了不重複建立 因為一個執行緒建立完後,會更改sizeCtl值

if ((tab = table) == null || tab.length == 0) {
//設定初始容量,給定了就設定為sc,反之為預設的大小16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//設定sc為n*0.75
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
//只有操作成功了才會返回結果tab
return tab;
}