註:本文源碼是JDK8的版本 ConcurrentHashMap 介紹(jdk 1.8)
ConcurrentHashMap是HashMap的升級版,HashMap是非安全執行緒的集合,ConcurrentHashMap則可以支援並行作業,
HashMap是我們平時開發過程中用的比較多的集合,ConcurrentHashMap就算用得少,但是聽過的肯定不少。
在jdk1.8 中HashMap是通過數組+鏈表+紅/黑樹狀結構實現的,ConcurrentHashMap 也是如此,在前面接觸的眾多演算法中,我們知道,要實現安全執行緒,要麼顯示的使用鎖來控制,這樣代碼會很簡單,要麼使用無鎖CAS演算法來實現,這樣代碼比較複雜,並發效能較好,很明顯ConcurrentHashMap 為了更好的並發的效能,選擇了CAS來實現,因此代碼相對就比較複雜了。
因為ConcurrentHashMap 和HashMap都是採用相同的資料結構,因此在分析ConcurrentHashMap 之前,最好是比較瞭解HashMap,這樣要容易理解一些,關於HashMap可以參考前面寫的內容: Java集合之HashMap源碼分析 資料結構
上面就是ConcurrentHashMap 的邏輯儲存結構,hash 表(table)用於散列,table的位置上可以儲存的是一個鏈表,也可能是一顆紅/黑樹狀結構。
從這個邏輯結構中,如果基於簡單的分析,我們可以知道,操作table 中不同位置上的資料(鏈表或者樹),是互相獨立的,也就是安全執行緒的,同時操作table 中同一個位置上的資料這個是需要同步,此外對table的大小調整也是需要進行同步處理的,這隻是一個簡單的分析,並不完全準確,下面我們一步一步來看ConcurrentHashMap 是如何做的。 鏈表節點–Node
//節點定義static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; //volatile修飾 ,保證可見度 volatile Node<K,V> next; Node(int hash, K key, V val, Node<K,V> next) { this.hash = hash; this.key = key; this.val = val; this.next = next; } //不允許直接改變value的值 public final V setValue(V value) { throw new UnsupportedOperationException(); } ... // 省略部分方法}
Node 為ConcurrentHashMap 的儲存單元,這個HashMap 中的Node是差不多的,只是這裡val,next都被volatile 修飾,保證了在多線程中的可見度。
下面是ConcurrentHashMap 中部分屬性的定義,有些和HashMap 中是一樣的。
/** * The array of bins. Lazily initialized upon first insertion. * Size is always a power of two. Accessed directly by iterators. */transient volatile Node<K,V>[] table; // hash 表 /** * The next table to use; non-null only while resizing. */private transient volatile Node<K,V>[] nextTable; // 用於擴容的,過渡表/** * The largest possible table capacity. This value must be * exactly 1<<30 to stay within Java array allocation and indexing * bounds for power of two table sizes, and is further required * because the top two bits of 32bit hash fields are used for * control purposes. */ // hash表的最大的size,必須是2的n次方這種private static final int MAXIMUM_CAPACITY = 1 << 30; /** * The default initial table capacity. Must be a power of 2 * (i.e., at least 1) and at most MAXIMUM_CAPACITY. */ //hash表的初始預設大小private static final int DEFAULT_CAPACITY = 16;/** * The load factor for this table. Overrides of this value in * constructors affect only the initial table capacity. The * actual floating point value isn't normally used -- it is * simpler to use expressions such as {@code n - (n >>> 2)} for * the associated resizing threshold. */private static final float LOAD_FACTOR = 0.75f;/** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2, and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. */ //鏈錶轉成紅/黑樹狀結構的閾值static final int TREEIFY_THRESHOLD = 8;/** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. *///紅/黑樹狀結構轉為鏈表的閾值static final int UNTREEIFY_THRESHOLD = 6;/** * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * The value should be at least 4 * TREEIFY_THRESHOLD to avoid * conflicts between resizing and treeification thresholds. *///儲存方式由鏈錶轉成紅/黑樹狀結構 table 容量的最小閾值static final int MIN_TREEIFY_CAPACITY = 64;/** * Minimum number of rebinnings per transfer step. Ranges are * subdivided to allow multiple resizer threads. This value * serves as a lower bound to avoid resizers encountering * excessive memory contention. The value should be at least * DEFAULT_CAPACITY. *///用於hash 表擴容後,搬移資料的步長(下面幾個屬性都是用於擴容或者控制sizeCtl 變數)private static final int MIN_TRANSFER_STRIDE = 16;/** * The number of bits used for generation stamp in sizeCtl. * Must be at least 6 for 32bit arrays. */private static int RESIZE_STAMP_BITS = 16;/** * The maximum number of threads that can help resize. * Must fit in 32 - RESIZE_STAMP_BITS bits. */private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;/** * The bit shift for recording size stamp in sizeCtl. */private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;/* * Encodings for Node hash fields. See above for explanation. */ static final int MOVED = -1; // 表示這是一個forwardNode節點static final int TREEBIN = -2; // 表示這時一個TreeBin節點// 用於產生hash值static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
上面的一些屬性,會結合後面的代碼來分析,這裡先有個大致瞭解就可以了。 紅/黑樹狀結構節點–TreeNode
static final class TreeNode<K,V> extends Node<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node<K,V> next, TreeNode<K,V> parent) { super(hash, key, val, next); this.parent = parent; } ... }
TreeNode 用於構建紅/黑樹狀結構節點,但是ConcurrentHashMap 中的TreeNode和HashMap中的TreeNode用途有點差別,HashMap中hash 表的部分位置上儲存的是一顆樹,具體儲存的就是TreeNode型的樹根節點,而ConcurrentHashMap 則不同,其hash 表是儲存的被TreeBin 封裝過的樹,也就是存放的是TreeBin對象,而不是TreeNode對象,同時TreeBin 帶有讀寫鎖,當需要調整樹時,為了保證線程的安全,必須上鎖。 TreeBin 對象
/*** TreeNodes used at the heads of bins. TreeBins do not hold user* keys or values, but instead point to list of TreeNodes and* their root. They also maintain a parasitic read-write lock* forcing writers (who hold bin lock) to wait for readers (who do* not) to complete before tree restructuring operations.*/static final class TreeBin<K,V> extends Node<K,V> { TreeNode<K,V> root; // 樹根 volatile TreeNode<K,V> first; // 樹的鏈式結構 volatile Thread waiter; // 等待者 volatile int lockState; // 鎖狀態 // values for lockState static final int WRITER = 1; // set while holding write lock static final int WAITER = 2; // set when waiting for write lock static final int READER = 4; // increment value for setting read lock...}
具體用法我們後面結合代碼再來分析。 過渡節點–ForwardingNode
/** * A node inserted at head of bins during transfer operations. */static final class ForwardingNode<K,V> extends Node<K,V> { final Node<K,V>[] nextTable; ForwardingNode(Node<K,V>[] tab) { super(MOVED, null, null, null); // hash 值為MOVED 進行標識 this.nextTable = tab; }
ForwardingNode 用於在hash 表擴容過程中的過渡節點,當hash 表進行擴容進行資料轉移的時候,其它線程如果還不斷的往原hash 表中添加資料,這個肯定是不好的,因此就引入了ForwardingNode 節點,當對原hash 表進行資料轉移時,如果hash 表中的位置還沒有被佔據,那麼就存放ForwardingNode 節點,表明現在hash 表進行中擴容轉移資料階段,這樣,其它線程在操作的時候,遇到ForwardingNode 節點,就知道hash 現在的狀態了,就可以協助參與hash 表的擴容過程。
到這裡,ConcurrentHashMap 中的重要的資料結構基本都瞭解了,一個是hash 表(table),一個是鏈表節點Node,其實呢就是紅/黑樹狀結構節點TreeNode. 構造方法
1、無參構造
/** * Creates a new, empty map with the default initial table size (16). */ public ConcurrentHashMap() { }
裡面什麼都沒有做,hash 表的初始化,是在第一次put 資料的時候初始化的。
2、指定hash 表大小
public ConcurrentHashMap(int initialCapacity) { if (initialCapacity < 0) throw new IllegalArgumentException(); int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); this.sizeCtl = cap;}/** * Returns a power of two table size for the given desired capacity. * See Hackers Delight, sec 3.2 */ //確保table的大小總是2的冪次方private static final int tableSizeFor(int c) { int n = c - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}
3、通過集合初始化
public ConcurrentHashMap(Map<? extends K, ? extends V> m) { this.sizeCtl = DEFAULT_CAPACITY; putAll(m); }
還要其他一些構造方法,這個可以自己去看,這裡就不完全列舉了。 put 資料
public V put(K key, V value) { return putVal(key, value, false);}
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); // 計算hash 值 int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; //將要初始化table if (tab == null || (n = tab.length) == 0) tab = initTable(); //通過hash 值計算table 中的索引,如果該位置沒有資料,則可以put else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // cas 將資料設定到table 中,如果設定成功,則本次put 基本完成 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } //如果table位置上的節點狀態時MOVE,則表明hash 進行中擴容搬移資料的過程中 else if ((fh = f.hash) == MOVED) //協助擴容 tab = helpTransfer(tab, f); else {// hash 表該位置上有資料,可能是鏈表,也可能是一顆樹 V oldVal = null; synchronized (f) { //將hash 表該位置進行上鎖,保證安全執行緒 // 上鎖後,只有再該位置資料和上鎖前一致才進行,否則需要重新迴圈 if (tabAt(tab, i) == f) { // hash 值>=0 表明這是一個鏈表結構 if (fh >= 0) { binCount = 1; // 遍曆鏈表 for (Node<K,V> e = f;; ++binCount) { K ek; // 存在相同的key,則覆蓋其value if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; // 不存在該key,將新資料添加到鏈表尾 if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } // 該位置是紅/黑樹狀結構,是TreeBin對象(注意是TreeBin,而不是TreeNode) else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; //通過TreeBin 中的方法,將資料添加到紅/黑樹狀結構中 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } //添加了資料,需要進行檢查 if (binCount != 0) { //if 成立,說明遍曆的是鏈表結構,並且超過了閥值,需要將鏈錶轉換為樹 if (binCount >= TREEIFY_THRESHOLD) //將table 索引i 的位置上的鏈錶轉換為紅/黑樹狀結構 treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // ConcurrentHashMap 容量增加1,檢查是否需要擴容 addCount(1L, binCount); return null;}
先來梳理一下大致邏輯:
1、計算key的hash 值
int hash = spread(key.hashCode());static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS;}
2、如果table(hash 表)沒有被初始化,則執行table的初始化過程。
3、通過hash 值得到table 中的具體位置i,如果該位置沒有資料,則直接將該資料存放在該位置。
4、如果table 表中該位置有資料,如果資料的hash 值為MOVED,則表明在進行table表的擴容工作,則輔助進行table的擴容和資料搬移工作。
5、如果table 表中該位置上的資料有效(儲存的真正的資料),鎖住該位置,然後執行後面的操作。
6、如果該位置上是鏈表,則遍曆鏈表,如果存在該key,則根據onlyIfAbsent 決定是否覆蓋該value,如果不存在該key,則添加到鏈表的末尾。
7、如果該位置上是樹形結構,則執行樹的插入操作。
8、如果資料添加到鏈表中,則需要檢查鏈表的長度是否超過了閥值,如果是則需要將該鏈錶轉換為紅/黑樹狀結構。
9、如果上面過程在多線程中,執行失敗(提前被其它線程改變),則需要從步驟2 重新開始。
10、遞增map的容量,並檢查是否需要擴容(addCount)。
從整體來看整個過程還是很清楚的,和HashMap有著大致相同的邏輯,因為ConcurrentHashMap 要保證安全執行緒,因此在存在競爭的操作上採用了cas 或者加鎖的方式進行,當執行失敗時,則需要重新開始,有了大致的認識後,接下來我們在挨著分析具體的步驟。 初始化table
在多線程下,必須要保證table的初始化只能執行一次。
/** * Initializes table, using the size recorded in sizeCtl. */private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { // 如果sizeCtl <0 則說明其它線程在初始化table,自己等待初始化完成即可 if ((sc = sizeCtl) < 0) // 讓出cpu Thread.yield(); // lost initialization race; just spin //將要執行table初始化,cas 設定 SIZECTL 值為-1 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); // 0.75*n } } finally { //這裡無需cas ,前面已經保證了只有一個線程能執行初始化工作 sizeCtl = sc; // sizeCtl 設定為0.75*n } break; } } return tab;}
sizeCtl預設為0,如果ConcurrentHashMap執行個體化時有容量參數,那麼sizeCtl會是一個2的冪次方的值。所以執行第一次put操作的線程會執行Unsafe.compareAndSwapInt方法修改sizeCtl為-1,有且只有一個線程能夠修改成功,其它線程通過Thread.yield()讓出CPU時間片等待table初始化完成,整個過程很清晰。
對於table 擴容部分,稍微後面再來分析,先來奠定點基礎。 添加資料到鏈表或樹
對於 putVal中synchronized 包裹的內容,就是將資料添加到鏈表或者紅/黑樹狀結構中,具體的代碼這裡就不再貼了,可以看前面的代碼。
在前面的屬性定義中有這樣的定義:如果是一顆紅/黑樹狀結構,那麼其根(也就是table中的資料)的hash 值為TREEBIN
static final int TREEBIN = -2; // hash for roots of trees
因此分析table 中某個位置上的資料的hash值,如果是負數則說明是一顆紅/黑樹狀結構,否則就是鏈表結構。
對於資料添加到鏈表中,這個應該很簡單,很容易就看懂了,其binCount 用於標識鏈表長度,其用法還是很巧妙的。
如果該位置是一顆樹,那麼其table 存放的就是TreeBin 對象,TreeBin 是中有樹根的引用同時擁有一系列的紅/黑樹狀結構的操作,因此直接調用TreeBin 對象的添加方法(putTreeVal)即可,對於紅/黑樹狀結構的操作部分,在本文不會多講,如果對其感興趣或者不太清楚的,可以參看另外兩篇的內容,裡面有對紅/黑樹狀結構的詳細介紹 親自動手畫紅/黑樹狀結構 Java集合之TreeMap源碼分析 將鏈錶轉換為紅/黑樹狀結構
當添加資料到鏈表中後,如果鏈表的長度超過了閥值,那麼會將鏈錶轉換為紅/黑樹狀結構。
/** * Replaces all linked nodes in bin at given index unless table is * too small, in which case resizes instead. */private final void treeifyBin(Node<K,V>[] tab, int index) { Node<K,V> b; int n, sc; if (tab != null) { // 如果table的容量不滿足鏈錶轉換為紅/黑樹狀結構的閥值要求,則需要對table 進行擴容 if ((n = tab.length) < MIN_TREEIFY_CAPACITY) tryPresize(n << 1); else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { // 將table 中該位置 鎖住 synchronized (b) { if (tabAt(tab, index) == b) { TreeNode<K,V> hd = null, tl = null; // 遍曆鏈表,構造TreeNode鏈表 for (Node<K,V> e = b; e != null; e = e.next) { TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null); if ((p.prev = tl) == null) hd = p; else //利用TreeNode 中的next域,構造TreeNode的鏈表 tl.next = p; tl = p; } //new TreeBin<K,V>(hd) 是將TreeNode 鏈表構造成一顆紅/黑樹狀結構 //將table原位置上的鏈表TreeNode 對象更改為TreeBin 對象 setTabAt(tab, index, new TreeBin<K,V>(hd)); } } } }}
大致過程:
1、如果table的容量不滿足鏈錶轉換為紅/黑樹狀結構的閥值要求,則需要對table 進行擴容
2、鎖住table 表該位置上的資料,遍曆鏈表,將Node 鏈錶轉換為TreeNode 鏈表
3、通過TreeBin對象通過TreeNode 鏈表構造紅/黑樹狀結構結構,樹根儲存在TreeBin對象中
4、將TreeBin Object Storage Service在table 原鏈表Node的位置上,至此鏈錶轉紅/黑樹狀結構完成。
將鏈錶轉換為紅/黑樹狀結構之前會構造TreeNode 鏈表,這樣在構造成紅/黑樹狀結構後,不僅可以通過樹的方式遍曆該結構,同時TreeNode 之間也存在一種鏈式結構,該結構就是最初的鏈錶轉換為紅/黑樹狀結構時構造的關係,在HashMap也存在這種結構,同時裡面有相關的圖示(在最後部分),可以參考: Java集合之HashMap源碼分析
在構造TreeBin 對象時,通過傳入的TreeNode 鏈表,會構造一顆紅/黑樹狀結構,這個可以看看TreeBin的構造方法,這裡就不貼出來了。 擴容
當table容量不足的時候,即table的元素數量達到容量閾值sizeCtl,需要對table進行擴容。
整個擴容分為兩部分: 構建一個nextTable,大小為table的兩倍。 把table的資料複製到nextTable中。
擴容在HashMap和ConcurrentHashMap 中都是重頭戲,ConcurrentHashMap是支援並發插入的,擴容操可以有兩種方式,一種是如同初始化table那樣,整個過程都控制只有一個線程進行操作,這樣肯定實現比較容易,但是這樣會影響到效能,當資料量比較大時,搬移資料將是一個費事操作,追求完美的jdk 當然不是那樣實現的,構建nextTable 這個肯定只有一個線程來執行,但是將table 中的資料複製到nextTable 中,這個可以進行並發複製,這樣的話,實現就比較複雜了,在無鎖的安全執行緒的演算法中,都用到了一種思想:輔助
在分析SynchronousQueue 中闡述過下面的一段話:
不使用鎖來保證資料結構的完整性,要確保其他線程不僅能夠判斷出第一個線程已經完成了更新還是處在更新的中途,還能夠判斷出如果第一個線程操作狀態,完成更新還需要什麼操作。如果線程發現了處在更新中途的資料結構,它就可以 “協助” 正在執行更新的線程完成更新,然後再進行自己的操作。當第一個線程回來試圖完成自己的更新時,會發現不再需要了,返回即可。
因此在table 複製資料的過程中,其它線程是可以參與一同進行複製的,這樣可以極大的提高效率,同時也必須要保證資料結構不被破壞,下面我們一步一步來看看是如何?這一過程的。
分析擴容,我們先從addCount 這個方法入手,當添加資料後,會遞增map中的size的計數,同時會檢查table 是否需要擴容
private final void addCount(long x,