《Java源碼分析》:ConcurrentHashMap JDK1.8
最近一直在看關於J.U.C中的源碼,瞭解原子操作,瞭解鎖機制,瞭解多線程並發等等。但是ConcurrentHashMap一直拖著到今天才算告一段落。
也要感謝ConcurrentHashMap這個類,剛開始就是想弄懂裡面的工作原理,但是,無奈看了網上關於介紹ConcurrentHashMap這個類的資料或部落格都是基於JDK1.8以前的,而剛好此類在JDK1.8之後有很大的變化。因此,由於裡面涉及到關於原子操作CAS,自己以前並不知道是什麼,於是就開始對原子操作進行瞭解,看了java.util.concurrent.atom包下相關類源碼對其有了一定的瞭解。接著為了瞭解鎖機制,看了java.util.concurrent.lock包下相關的類庫,對鎖機制有了大概的瞭解之後,看了線程池相關的類,對線程池也有了一定的瞭解。
關於阻塞隊列相關的類,自己也大致看了下,但是並沒有形成相應的博文,以後有時間重新來瞭解他們的時候才記錄吧。整個過程大概花費了我將近一個來月的時間,雖然對看過的類庫的內部實現都只是一個大致的瞭解,但是確實收穫還是挺多的。讓我們更好的明白在多線程並發中他們是如何來工作的。
回到正題,剛好藉著今天星期天,花了將近一天的時間來看ConcurrentHashMap的實現原理,總算看了一個大概,有了一個大致的瞭解。也就有了這篇博文。 ConcurrentHashMap 在JDK1.8版本以前的實現原理
既然本篇博文的標題明確的標出了是基於JDK1.8版本的,也就暗示了這個版本和以前的版本關於ConcurrentHashMap有些許的不同,對吧。x
下面我們就先藉助網上的資料來看下以前版本的ConcurrentHashMap的實現思路。
我們都知道HashMap是線程不安全的。Hashtable是安全執行緒的。看過Hashtable源碼的我們都知道Hashtable的安全執行緒是採用在每個方法來添加了synchronized關鍵字來修飾,即Hashtable是針對整個table的鎖定,這樣就導致HashTable容器在競爭激烈的並發環境下表現出效率低下。
效率低下的原因說的更詳細點:是因為所有訪問HashTable的線程都必須競爭同一把鎖。當一個線程訪問HashTable的同步方法時,其他線程訪問HashTable的同步方法時,可能會進入阻塞或輪詢狀態。如線程1使用put進行添加元素,線程2不但不能使用put方法添加元素,並且也不能使用get方法來擷取元素,所以競爭越激烈效率越低。
基於Hashtable的缺點,人們就開始思考,假如容器裡有多把鎖,每一把鎖用於鎖容器其中一部分資料,那麼當多線程訪問容器裡不同資料區段的資料時,線程間就不會存在鎖競爭,從而可以有效提高並發訪問效率呢。。這就是我們的“鎖分離”技術,這也是ConcurrentHashMap實現的基礎。
ConcurrentHashMap使用的就是鎖分段技術,ConcurrentHashMap由多個Segment組成(Segment下包含很多Node,也就是我們的索引值對了),每個Segment都有把鎖來實現安全執行緒,當一個線程佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他線程訪問。
因此,關於ConcurrentHashMap就轉化為了對Segment的研究。這是因為,ConcurrentHashMap的get、put操作是直接委託給Segment的get、put方法,但是自己上手上的JDK1.8的具體實現確不想網上這些博文所介紹的。因此,就有了本篇博文的介紹。
推薦幾個JDK1.8以前版本的關於ConcurrentHashMap的原理分析,方便大家比較。
1、http://www.iteye.com/topic/344876
2、http://ifeve.com/concurrenthashmap/
如需要更多,請自己網上搜尋即可。
下面就開始JDK1.8版本中ConcurrentHashMap的介紹。 JDK1.8 版本中ConcurrentHashMap介紹 1、前言
首先要說明的幾點:
1、JDK1.8的ConcurrentHashMap中Segment雖保留,但已經簡化屬性,僅僅是為了相容舊版本。
2、ConcurrentHashMap的底層與Java1.8的HashMap有相通之處,底層依然由“數組”+鏈表+紅/黑樹狀結構來實現的,底層結構存放的是TreeBin對象,而不是TreeNode對象;
3、ConcurrentHashMap實現中借用了較多的CAS演算法,unsafe.compareAndSwapInt(this, valueOffset, expect, update); CAS(Compare And Swap),意思是如果valueOffset位置包含的值與expect值相同,則更新valueOffset位置的值為update,並返回true,否則不更新,返回false。
ConcurrentHashMap既然藉助了CAS來實現非阻塞的無鎖實現安全執行緒,那麼是不是就沒有用鎖了呢。。答案:還是使用了synchronized關鍵字進行同步了的,在哪裡使用了呢。在操作hash值相同的鏈表的頭結點還是會synchronized上鎖,這樣才能保證安全執行緒。
看完ConcurrentHashMap整個類的源碼,給自己的感覺就是和HashMap的實現基本一模一樣,當有修改操作時藉助了synchronized來對table[i]進行鎖定保證了安全執行緒以及使用了CAS來保證原子性操作,其它的基本一致,例如:ConcurrentHashMap的get(int key)方法的實現思路為:根據key的hash值找到其在table所對應的位置i,然後在table[i]位置所儲存的鏈表(或者是樹)進行尋找是否有鍵為key的節點,如果有,則返回節點對應的value,否則返回null。思路是不是很熟悉,是不是和HashMap中該方法的思路一樣。所以,如果你也在看ConcurrentHashMap的源碼,不要害怕,思路還是原來的思路,只是多了些許東西罷了。 2、ConcurrentHashMap類中相關屬性的介紹
為了方便介紹此類後面的實現,這裡需要先將此類中的一些屬性給介紹下。
sizeCtl最重要的屬性之一,看源碼之前,這個屬性工作表示什麼意思,一定要記住。
0、private transient volatile int sizeCtl;//控制標識符
此屬性在源碼中給出的注釋如下:
/** * Table initialization and resizing control. When negative, the * table is being initialized or resized: -1 for initialization, * else -(1 + the number of active resizing threads). Otherwise, * when table is null, holds the initial table size to use upon * creation, or 0 for default. After initialization, holds the * next element count value upon which to resize the table. */
翻譯如下:
sizeCtl是控制標識符,不同的值表示不同的意義。 負數代表進行中初始化或擴容操作 ,其中-1代表正在初始化 ,-N 表示有N-1個線程進行中擴容操作 正數或0代表hash表還沒有被初始化,這個數值表示初始化或下一次進行擴容的大小,類似於擴容閾值。它的值始終是當前ConcurrentHashMap容量的0.75倍,這與loadfactor是對應的。實際容量>=sizeCtl,則擴容。
1、 transient volatile Node<K,V>[] table;是一個容器數組,第一次插入資料的時候初始化,大小是2的冪次方。這就是我們所說的底層結構:”數組+鏈表(或樹)”
2、private static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
3、private static final intDEFAULT_CAPACITY = 16;
4、static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // MAX_VALUE=2^31-1=2147483647
5、private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;
6、private static final float LOAD_FACTOR = 0.75f;
7、static final int TREEIFY_THRESHOLD = 8; // 鏈錶轉樹的閥值,如果table[i]下面的鏈表長度大於8時就轉化為數
8、static final int UNTREEIFY_THRESHOLD = 6; //樹轉鏈表的閥值,小於等於6是轉為鏈表,僅在擴容tranfer時才可能樹轉鏈表
9、static final int MIN_TREEIFY_CAPACITY = 64;
10、private static final int MIN_TRANSFER_STRIDE = 16;
11、private static int RESIZE_STAMP_BITS = 16;
12、private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; // help resize的最大線程數
13、private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
14、static final int MOVED = -1; // hash for forwarding nodes(forwarding nodes的hash值)、標示位
15、static final int TREEBIN = -2; // hash for roots of trees(樹根節點的hash值)
16、static final int RESERVED = -3; // hash for transient reservations(ReservationNode的hash值) 3、ConcurrentHashMap的建構函式
和往常一樣,我們還是從類的建構函式開始說起。
/** * Creates a new, empty map with the default initial table size (16). */ public ConcurrentHashMap() { } 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; } /* * Creates a new map with the same mappings as the given map. * */ public ConcurrentHashMap(Map<? extends K, ? extends V> m) { this.sizeCtl = DEFAULT_CAPACITY; putAll(m); } public ConcurrentHashMap(int initialCapacity, float loadFactor) { this(initialCapacity, loadFactor, 1); } public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); if (initialCapacity < concurrencyLevel) // Use at least as many bins initialCapacity = concurrencyLevel; // as estimated threads long size = (long)(1.0 + (long)initialCapacity / loadFactor); int cap = (size >= (long)MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)size); this.sizeCtl = cap; }
有過HashMap和Hashtable源碼經曆,看這些建構函式是不是相當easy哈。
上面的建構函式主要幹了兩件事:
1、參數的有效性檢查
2、table初始化的長度(如果不指定預設情況下為16)。
這裡要說一個參數:concurrencyLevel,表示能夠同時更新ConccurentHashMap且不產生鎖競爭的最大線程數。預設值為16,(即允許16個線程並發可能不會產生競爭)。為了保證並發的效能,我們要很好的估計出concurrencyLevel值,不然要麼競爭相當厲害,從而導致線程試圖寫入當前鎖定的段時阻塞。 ConcurrentHashMap類中相關節點類:Node/TreeNode/TreeBin 1、Node類
Node類是table數組中的儲存元素,即一個Node對象就代表一個索引值對(key,value)儲存在table中。
Node類是沒有提供修改入口的(唯一的setValue方法拋異常),因此只能用於唯讀遍曆。
此類的具體代碼如下:
/* *Node類是沒有提供修改入口的(setValue方法拋異常,供子類實現), 即是可讀的。只能用於唯讀遍曆。 */ 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; } public final K getKey() { return key; } public final V getValue() { return val; } /* HashMap中Node類的hashCode()方法中的代碼為:Objects.hashCode(key) ^ Objects.hashCode(value) 而Objects.hashCode(key)最終也是調用了 key.hashCode(),因此,效果一樣。寫法不一樣罷了 */; public final int hashCode() { return key.hashCode() ^ val.hashCode(); } public final String toString(){ return key + "=" + val; } public final V setValue(V value) { // 不允許修改value值,HashMap允許 throw new UnsupportedOperationException(); } /* HashMap使用if (o == this),且嵌套if;ConcurrentHashMap使用&& 個人覺得HashMap格式的代碼更好閱讀和理解 */ public final boolean equals(Object o) { Object k, v, u; Map.Entry<?,?> e; return ((o instanceof Map.Entry) && (k = (e = (Map.Entry<?,?>)o).getKey()) != null && (v = e.getValue()) != null && (k == key || k.equals(key)) && (v == (u = val) || v.equals(u))); } /* * Virtualized support for map.get(); overridden in subclasses. *增加find方法輔助get方法 ,HashMap中的Node類中沒有此方法 */ Node<K,V> find(int h, Object k) { Node<K,V> e = this; if (k != null) { do { K ek; if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) return e; } while ((e = e.next) != null); } return null; } }
我們在看這個類時,可以與HashMap中的Node類的具體代碼進行比較,發現在具體的實現上,有一定的細微的區別。
例如:在ConcurrentHashMap.Node的hashCode的代碼是這樣的:
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
而HashMap.Node的hashCode的代碼是這樣的:
public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); }
而Objects.hashCode(key)最終也是調用了 key.hashCode(),因此,兩者的效果一樣,寫法不一樣罷了。
除了hashCode方法有一點差別,Node類中的find方法在兩個類的實現中的寫法也不一樣。 2、TreeNode
/* * Nodes for use in TreeBins */ 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; } Node<K,V> find(int h, Object k) { return findTreeNode(h, k, null); } /* * Returns the TreeNode (or null if not found) for the given key * starting at given root. *根據給定的key值從root節點出發找出節點 * */ final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) { if (k != null) {//HashMap沒有非空判斷 TreeNode<K,V> p = this; do { int ph, dir; K pk; TreeNode<K,V> q; TreeNode<K,V> pl = p.left, pr = p.right; if ((ph = p.hash) > h) p = pl; else if (ph < h) p = pr; else if ((pk = p.key) == k || (pk != null && k.equals(pk))) return p; else if (pl == null) p = pr; else if (pr == null) p = pl; else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0) p = (dir < 0) ? pl : pr; else if ((q = pr.findTreeNode(h, k, kc)) != null) return q; else p = pl; } while (p != null); } return null; } }
和HashMap相比,這裡的TreeNode相當簡潔;ConcurrentHashMap鏈錶轉樹時,並不會直接轉,
正如注釋(Nodes for use in TreeBins)所說,只是把這些節點封裝成TreeNode放到TreeBin中,
再由TreeBin來轉化紅/黑樹狀結構。紅/黑樹狀結構不理解沒關係,並不影響看ConcurrentHashMap的內部實現 3、TreeBins
TreeBin用於封裝維護TreeNode,包含putTreeVal、lookRoot、UNlookRoot、remove、balanceInsetion、balanceDeletion等方法,當鏈錶轉樹時,用於封裝TreeNode,也就是說,ConcurrentHashMap的紅/黑樹狀結構存放的時TreeBin,而不是treeNode。
TreeBins類代碼太長,截取部分代碼如下:
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 /** * Creates bin with initial set of nodes headed by b. */ TreeBin(TreeNode<K,V> b) { super(TREEBIN, null, null, null); this.first = b; TreeNode<K,V> r = null; for (TreeNode<K,V> x = b, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; if (r == null) { x.parent = null; x.red = false; r = x; } else { K k = x.key; int h = x.hash; Class<?> kc = null; for (TreeNode<K,V> p = r;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x; r = balanceInsertion(r, x); break; } } } } this.root = r; assert checkInvariants(root); } //........other methods }
5、ForwardingNode:在transfer操作中,將一個節點插入到桶中
/* * A node inserted at head of bins during transfer operations. *在transfer操作中,一個節點插入到bins中 */ static final class ForwardingNode<K,V> extends Node<K,V> { final Node<K,V>[] nextTable; ForwardingNode(Node<K,V>[] tab) { //Node(int hash, K key, V val, Node<K,V> next)是Node類的建構函式 super(MOVED, null, null, null); this.nextTable = tab; } Node<K,V> find(int h, Object k) { // loop to avoid arbitrarily deep recursion on forwarding nodes outer: for (Node<K,V>[] tab = nextTable;;) { Node<K,V> e; int n; if (k == null || tab == null || (n = tab.length) == 0 || (e = tabAt(tab, (n - 1) & h)) == null) return null; for (;;) { int eh; K ek; if ((eh = e.hash) == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) return e; if (eh < 0) { if (e instanceof ForwardingNode) { tab = ((ForwardingNode<K,V>)e).nextTable; continue outer; } else return e.find(h, k); } if ((e = e.next) == null) return null; } } } }
ConcurrentHashMap類中的put(K key, V value)方法的原理分析
我們對Node、TreeNode、TreeBin有一點認識後,我們就可以看下ConcurrentHashMap類的put方法是如何來實現的了,這裡給出一個建議,關於容器我們用的最多的就是put、get方法了,我們看源碼的實現,我們核心要關注的就是put、get方法的實現,只要我們弄懂這兩個方法實現,這個類的大概實現思想我們也就知道了哈
基於此,我們就先來看ConcurrentHashMap類的put方法
put(K key, V value)方法的功能:將制定的索引值對映射到table中,key/value均不能為null
put方法的代碼如下:
public V put(K key, V value) { return putVal(key, value, false); }
由於直接是調用了putVal(key, value, false)方法,那就我們就繼續看。
putVal(key, value, false)方法的代碼如下:
/** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode());//計算hash值,兩次hash操作 int binCount = 0; for (Node<K,V>[] tab = table;;) {//類似於while(true),死迴圈,直到插入成功 Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0)//檢查是否初始化了,如果沒有,則初始化 tab = initTable(); /* i=(n-1)&hash 等價於i=hash%n(前提是n為2的冪次方).即取出table中位置的節點用f表示。 有如下兩種情況: 1、如果table[i]==null(即該位置的節點為空白,沒有發生碰撞),則利用CAS操作直接儲存在該位置, 如果CAS操作成功則退出死迴圈。 2、如果table[i]!=null(即該位置已經有其它節點,發生碰撞) */ else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { 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)//檢查table[i]的節點的hash是否等於MOVED,如果等於,則檢測到正在擴容,則協助其擴容 tab = helpTransfer(tab, f);//協助其擴容 else {//運行到這裡,說明table[i]的節點的hash值不等於MOVED。 V oldVal = null; synchronized (f) {//鎖定,(hash值相同的鏈表的前端節點) if (tabAt(tab, i) == f) {//避免多線程,需要重新檢查 if (fh >= 0) {//鏈表節點 binCount = 1; /* 下面的代碼就是先尋找鏈表中是否出現了此key,如果出現,則更新value,並跳出迴圈, 否則將節點加入到裡阿尼報末尾並跳出迴圈 */ for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent)//僅putIfAbsent()方法中onlyIfAbsent為true