1.0 數據結構
- 拋棄了 JDK 1.7 中原有的 Segment 分段鎖,而采用了 CAS + synchronized 來保證並發安全性。
- 將 JDK 1.7 中存放數據的 HashEntry 改為 Node,但作用是相同的。
2.0 put方法
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); // 鍵或值為空,拋出異常 // 鍵的hash值經過計算獲得hash值,這里的 hash 計算多了一步 & HASH_BITS,HASH_BITS 是 0x7fffffff,該步是為了消除最高位上的負符號 hash的負在ConcurrentHashMap中有特殊意義表示在擴容或者是樹結點 int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { // 無限循環 Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) // 表為空或者表的長度為0 // 初始化表 tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 表不為空並且表的長度大於0,並且該桶不為空 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) // 比較並且交換值,如tab的第i項為空則用新生成的node替換 break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) // 該結點的hash值為MOVED // 進行結點的轉移(在擴容的過程中) tab = helpTransfer(tab, f); else { V oldVal = null; synchronized (f) { // 加鎖同步 if (tabAt(tab, i) == f) { // 找到table表下標為i的結點 if (fh >= 0) { // 該table表中該結點的hash值大於0 // binCount賦值為1 binCount = 1; for (Node<K,V> e = f;; ++binCount) { // 無限循環 K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 結點的hash值相等並且key也相等 // 保存該結點的val值 oldVal = e.val; if (!onlyIfAbsent) // 進行判斷 // 將指定的value保存至結點,即進行了結點值的更新 e.val = value; break; } // 保存當前結點 Node<K,V> pred = e; if ((e = e.next) == null) { // 當前結點的下一個結點為空,即為最后一個結點 // 新生一個結點並且賦值給next域 pred.next = new Node<K,V>(hash, key, value, null); // 退出循環 break; } } } else if (f instanceof TreeBin) { // 結點為紅黑樹結點類型 Node<K,V> p; // binCount賦值為2 binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { // 將hash、key、value放入紅黑樹 // 保存結點的val oldVal = p.val; if (!onlyIfAbsent) // 判斷 // 賦值結點value值 p.val = value; } } } } if (binCount != 0) { // binCount不為0 if (binCount >= TREEIFY_THRESHOLD) // 如果binCount大於等於轉化為紅黑樹的閾值 // 進行轉化 treeifyBin(tab, i); if (oldVal != null) // 舊值不為空 // 返回舊值 return oldVal; break; } } } // 增加binCount的數量 addCount(1L, binCount); return null; }
put方法總結
- 判斷存儲的 key、value 是否為空,若為空,則拋出異常,否則,進入步驟 2。
- 計算 key 的 hash 值,隨后進入自旋,該自旋可以確保成功插入數據,若 table 表為空或者長度為 0,則初始化 table 表,否則,進入步驟 3。
- 根據 key 的 hash 值取出 table 表中的結點元素,若取出的結點為空(該桶為空),則使用 CAS 將 key、value、hash 值生成的結點放入桶中。否則,進入步驟 4。
- 若該結點的的 hash 值為 MOVED(-1),則對該桶中的結點進行轉移,否則,進入步驟 5。
- 對桶中的第一個結點(即 table 表中的結點)進行加鎖,對該桶進行遍歷,桶中的結點的 hash 值與 key 值與給定的 hash 值和 key 值相等,則根據標識選擇是否進行更新操作(用給定的 value 值替換該結點的 value 值),若遍歷完桶仍沒有找到 hash 值與 key 值和指定的 hash 值與 key 值相等的結點,則直接新生一個結點並賦值為之前最后一個結點的下一個結點。進入步驟 6。
- 若 binCount 值達到紅黑樹轉化的閾值,則將桶中的結構轉化為紅黑樹存儲,最后,增加 binCount 的值。
- 如果桶中的第一個元素的 hash 值大於 0,說明是鏈表結構,則對鏈表插入或者更新。
- 如果桶中的第一個元素是 TreeBin,說明是紅黑樹結構,則按照紅黑樹的方式進行插入或者更新。
- 在鎖的保護下,插入或者更新完畢后,如果是鏈表結構,需要判斷鏈表中元素的數量是否超過 8(默認),一旦超過,就需要考慮進行數組擴容,或者是鏈表轉紅黑樹。
3.0 初始化
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { // 初始化數組的工作其它線程正在做 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin // CAS 一下,將 sizeCtl 設置為 -1,代表搶到了鎖 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { // DEFAULT_CAPACITY 默認初始容量是 16 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 初始化數組,長度為 16 或初始化時提供的長度 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; // 將這個數組賦值給 table,table 是 volatile 的 table = tab = nt; // 如果 n 為 16 的話,那么這里 sc = 12 // 其實就是 0.75 * n sc = n - (n >>> 2); } } finally { // 設置 sizeCtl 為 sc sizeCtl = sc; } break; } } return tab; }
4.0 鏈表轉紅黑樹
private final void treeifyBin(Node<K,V>[] tab, int index) { Node<K,V> b; int n, sc; if (tab != null) { // 表不為空 if ((n = tab.length) < MIN_TREEIFY_CAPACITY) // table表的長度小於最小的長度 // 進行擴容,調整某個桶中結點數量過多的問題(由於某個桶中結點數量超出了閾值,則觸發treeifyBin) tryPresize(n << 1); else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { // 桶中存在結點並且結點的hash值大於等於0 synchronized (b) { // 對桶中第一個結點進行加鎖 if (tabAt(tab, index) == b) { // 第一個結點沒有變化 TreeNode<K,V> hd = null, tl = null; for (Node<K,V> e = b; e != null; e = e.next) { // 遍歷桶中所有結點 // 新生一個TreeNode結點 TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null); if ((p.prev = tl) == null) // 該結點前驅為空 // 設置p為頭結點 hd = p; else // 尾結點的next域賦值為p tl.next = p; // 尾結點賦值為p tl = p; } // 設置table表中下標為index的值為hd setTabAt(tab, index, new TreeBin<K,V>(hd)); } } } } }
數組擴容
// 參數 size 傳進來的時候就已經翻倍(例如 16) private final void tryPresize(int size) { // c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。 // 16 + 8 + 1 -> 32 -> 2^8 int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1); int sc; while ((sc = sizeCtl) >= 0) { Node<K,V>[] tab = table; int n; // 這個 if 分支和之前說的初始化數組的代碼基本上是一樣的,在這里,我們可以不用管這塊代碼 if (tab == null || (n = tab.length) == 0) { n = (sc > c) ? sc : c; if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if (table == tab) { @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = nt; sc = n - (n >>> 2); // 0.75 * n } } finally { sizeCtl = sc; } } } else if (c <= sc || n >= MAXIMUM_CAPACITY) break; else if (tab == table) { int rs = resizeStamp(n); if (sc < 0) { Node<K,V>[] nt; if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; // 2. 用 CAS 將 sizeCtl 加 1,然后執行 transfer 方法 // 此時 nextTab 不為 null if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt); } // 1. 將 sizeCtl 設置為 (rs << RESIZE_STAMP_SHIFT) + 2) // 調用 transfer 方法,此時 nextTab 參數為 null else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); } } }
5.0 get方法
public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; int h = spread(key.hashCode()); if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { // 判斷頭結點是否就是我們需要的結點 if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } // 如果頭結點的 hash 小於 0,說明 正在擴容,或者該位置是紅黑樹 else if (eh < 0) // 參考 ForwardingNode.find(int h, Object k) 和 TreeBin.find(int h, Object k) return (p = e.find(h, key)) != null ? p.val : null; // 遍歷鏈表 while ((e = e.next) != null) { if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; }
6.0 其他問題
6.2.1 ConcurrentHashmap 不支持 key 或者 value 為 null 的原因
- ConcurrentHashmap 和 Hashtable 都是支持並發的,當通過
get(k)
獲取對應的 value 時,如果獲取到的是 null 時,無法判斷是put(k,v)
的時候 value 為 null,還是這個 key 從來沒有做過映射。- HashMap 是非並發的,可以通過 contains(key) 來做這個判斷。
- 支持並發的 Map 在調用
m.contains(key)
和m.get(key)
時,m 可能已經發生了更改。
- 因此 ConcurrentHashmap 和 Hashtable 都不支持 key 或者 value 為 null。