併發編程實踐中,ConcurrentHashMap是一個常常被使用的數據結構,相比於Hashtable以及Collections.synchronizedMap(),ConcurrentHashMap在線程安全的基礎上提供了更好的寫併發能力,但同時下降了對讀一致性的要求(這點好像CAP理論啊 O(∩_∩)O)。ConcurrentHashMap的設計與實現很是精巧,大量的利用了volatile,final,CAS等lock-free技術來減小鎖競爭對於性能的影響,不管對於Java併發編程的學習仍是Java內存模型的理解,ConcurrentHashMap的設計以及源碼都值得很是仔細的閱讀與揣摩。html
這篇日誌記錄了本身對ConcurrentHashMap的一些總結,因爲JDK6,7,8中實現都不一樣,須要分開闡述在不一樣版本中的ConcurrentHashMap。node
本文從源碼出發,挑選我的以爲重要的點(會用紅色標註)再次進行回顧,以及闡述ConcurrentHashMap的一些注意點。算法
ConcurrentHashMap採用了分段鎖的設計,只有在同一個分段內才存在競態關係,不一樣的分段鎖之間沒有鎖競爭。相比於對整個Map加鎖的設計,分段鎖大大的提升了高併發環境下的處理能力。但同時,因爲不是對整個Map加鎖,致使一些須要掃描整個Map的方法(如size(), containsValue())須要使用特殊的實現,另一些方法(如clear())甚至放棄了對一致性的要求(ConcurrentHashMap是弱一致性的,具體請查看ConcurrentHashMap能徹底替代HashTable嗎?)。編程
ConcurrentHashMap中的分段鎖稱爲Segment,它即相似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每一個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。ConcurrentHashMap中的HashEntry相對於HashMap中的Entry有必定的差別性:HashEntry中的value以及next都被volatile修飾,這樣在多線程讀寫過程當中可以保持它們的可見性,代碼以下:數組
static final class HashEntry<K,V> { final int hash; final K key; volatile V value; volatile HashEntry<K,V> next; }
併發度能夠理解爲程序運行時可以同時更新ConccurentHashMap且不產生鎖競爭的最大線程數,實際上就是ConcurrentHashMap中的分段鎖個數,即Segment[]的數組長度。ConcurrentHashMap默認的併發度爲16,但用戶也能夠在構造函數中設置併發度。當用戶設置併發度時,ConcurrentHashMap會使用大於等於該值的最小2冪指數做爲實際併發度(假如用戶設置併發度爲17,實際併發度則爲32)。運行時經過將key的高n位(n = 32 – segmentShift)和併發度減1(segmentMask)作位與運算定位到所在的Segment。segmentShift與segmentMask都是在構造過程當中根據concurrency level被相應的計算出來。緩存
若是併發度設置的太小,會帶來嚴重的鎖競爭問題;若是併發度設置的過大,本來位於同一個Segment內的訪問會擴散到不一樣的Segment中,CPU cache命中率會降低,從而引發程序性能降低。(文檔的說法是根據你併發的線程數量決定,太多會導性能下降)安全
和JDK6不一樣,JDK7中除了第一個Segment以外,剩餘的Segments採用的是延遲初始化的機制:每次put以前都須要檢查key對應的Segment是否爲null,若是是則調用ensureSegment()以確保對應的Segment被建立。數據結構
ensureSegment可能在併發環境下被調用,但與想象中不一樣,ensureSegment並未使用鎖來控制競爭,而是使用了Unsafe對象的getObjectVolatile()提供的原子讀語義結合CAS來確保Segment建立的原子性。代碼段以下:多線程
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck Segment<K,V> s = new Segment<K,V>(lf, threshold, tab); while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) break; } }
和JDK6同樣,ConcurrentHashMap的put方法被代理到了對應的Segment(定位Segment的原理以前已經描述過)中。與JDK6不一樣的是,JDK7版本的ConcurrentHashMap在得到Segment鎖的過程當中,作了必定的優化 - 在真正申請鎖以前,put方法會經過tryLock()方法嘗試得到鎖,在嘗試得到鎖的過程當中會對對應hashcode的鏈表進行遍歷,若是遍歷完畢仍然找不到與key相同的HashEntry節點,則爲後續的put操做提早建立一個HashEntry。當tryLock必定次數後仍沒法得到鎖,則經過lock申請鎖。併發
須要注意的是,因爲在併發環境下,其餘線程的put,rehash或者remove操做可能會致使鏈表頭結點的變化,所以在過程當中須要進行檢查,若是頭結點發生變化則從新對錶進行遍歷。而若是其餘線程引發了鏈表中的某個節點被刪除,即便該變化由於是非原子寫操做(刪除節點後連接後續節點調用的是Unsafe.putOrderedObject(),該方法不提供原子寫語義)可能致使當前線程沒法觀察到,但由於不影響遍歷的正確性因此忽略不計。
之因此在獲取鎖的過程當中對整個鏈表進行遍歷,主要目的是但願遍歷的鏈表被CPU cache所緩存,爲後續實際put過程當中的鏈表遍歷操做提高性能。
在得到鎖以後,Segment對鏈表進行遍歷,若是某個HashEntry節點具備相同的key,則更新該HashEntry的value值,不然新建一個HashEntry節點,將它設置爲鏈表的新head節點並將原頭節點設爲新head的下一個節點。新建過程當中若是節點總數(含新建的HashEntry)超過threshold,則調用rehash()方法對Segment進行擴容,最後將新建HashEntry寫入到數組中。
put方法中,連接新節點的下一個節點(HashEntry.setNext())以及將鏈表寫入到數組中(setEntryAt())都是經過Unsafe的putOrderedObject()方法來實現,這裏並未使用具備原子寫語義的putObjectVolatile()的緣由是:JMM會保證得到鎖到釋放鎖之間全部對象的狀態更新都會在鎖被釋放以後更新到主存,從而保證這些變動對其餘線程是可見的。
相對於HashMap的resize,ConcurrentHashMap的rehash原理相似,可是Doug Lea爲rehash作了必定的優化,避免讓全部的節點都進行復制操做:因爲擴容是基於2的冪指來操做,假設擴容前某HashEntry對應到Segment中數組的index爲i,數組的容量爲capacity,那麼擴容後該HashEntry對應到新數組中的index只可能爲i或者i+capacity,所以大多數HashEntry節點在擴容先後index能夠保持不變。基於此,rehash方法中會定位第一個後續全部節點在擴容後index都保持不變的節點,而後將這個節點以前的全部節點重排便可。這部分代碼以下:
private void rehash(HashEntry<K,V> node) { HashEntry<K,V>[] oldTable = table; int oldCapacity = oldTable.length; int newCapacity = oldCapacity << 1; threshold = (int)(newCapacity * loadFactor); HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[newCapacity]; int sizeMask = newCapacity - 1; for (int i = 0; i < oldCapacity ; i++) { HashEntry<K,V> e = oldTable[i]; if (e != null) { HashEntry<K,V> next = e.next; int idx = e.hash & sizeMask; if (next == null) // Single node on list newTable[idx] = e; else { // Reuse consecutive sequence at same slot HashEntry<K,V> lastRun = e; int lastIdx = idx; for (HashEntry<K,V> last = next; last != null; last = last.next) { int k = last.hash & sizeMask; if (k != lastIdx) { lastIdx = k; lastRun = last; } } newTable[lastIdx] = lastRun; // Clone remaining nodes for (HashEntry<K,V> p = e; p != lastRun; p = p.next) { V v = p.value; int h = p.hash; int k = h & sizeMask; HashEntry<K,V> n = newTable[k]; newTable[k] = new HashEntry<K,V>(h, p.key, v, n); } } } } int nodeIndex = node.hash & sizeMask; // add the new node node.setNext(newTable[nodeIndex]); newTable[nodeIndex] = node; table = newTable; }
和put相似,remove在真正得到鎖以前,也會對鏈表進行遍歷以提升緩存命中率。
get與containsKey兩個方法幾乎徹底一致:他們都沒有使用鎖,而是經過Unsafe對象的getObjectVolatile()方法提供的原子讀語義,來得到Segment以及對應的鏈表,而後對鏈表遍歷判斷是否存在key相同的節點以及得到該節點的value。但因爲遍歷過程當中其餘線程可能對鏈表結構作了調整,所以get和containsKey返回的多是過期的數據,這一點是ConcurrentHashMap在弱一致性上的體現。若是要求強一致性,那麼必須使用Collections.synchronizedMap()方法。
這些方法都是基於整個ConcurrentHashMap來進行操做的,他們的原理也基本相似:首先不加鎖循環執行如下操做:循環全部的Segment(經過Unsafe的getObjectVolatile()以保證原子讀語義),得到對應的值以及全部Segment的modcount之和。若是連續兩次全部Segment的modcount和相等,則過程當中沒有發生其餘線程修改ConcurrentHashMap的狀況,返回得到的值。
當循環次數超過預約義的值時,這時須要對全部的Segment依次進行加鎖,獲取返回值後再依次解鎖。值得注意的是,加鎖過程當中要強制建立全部的Segment,不然容易出現其餘線程建立Segment並進行put,remove等操做。代碼以下:
for(int j =0; j < segments.length; ++j) ensureSegment(j).lock();// force creation
通常來講,應該避免在多線程環境下使用size和containsValue方法。
注1:modcount在put, replace, remove以及clear等方法中都會被修改。
注2:對於containsValue方法來講,若是在循環過程當中發現匹配value的HashEntry,則直接返回true。
最後,與HashMap不一樣的是,ConcurrentHashMap並不容許key或者value爲null,按照Doug Lea的說法,這麼設計的緣由是在ConcurrentHashMap中,一旦value出現null,則表明HashEntry的key/value沒有映射完成就被其餘線程所見,須要特殊處理。在JDK6中,get方法的實現中就有一段對HashEntry.value == null的防護性判斷。但Doug Lea也認可實際運行過程當中,這種狀況彷佛不可能發生(參考:http://cs.oswego.edu/pipermai...)。
ConcurrentHashMap在JDK8中進行了巨大改動,很須要經過源碼來再次學習下Doug Lea的實現方法。
它摒棄了Segment(鎖段)的概念,而是啓用了一種全新的方式實現,利用CAS算法。它沿用了與它同時期的HashMap版本的思想,底層依然由「數組」+鏈表+紅黑樹的方式思想(JDK7與JDK8中HashMap的實現),可是爲了作到併發,又增長了不少輔助的類,例如TreeBin,Traverser等對象內部類。
首先來看幾個重要的屬性,與HashMap相同的就再也不介紹了,這裏重點解釋一下sizeCtl這個屬性。能夠說它是ConcurrentHashMap中出鏡率很高的一個屬性,由於它是一個控制標識符,在不一樣的地方有不一樣用途,並且它的取值不一樣,也表明不一樣的含義。
/** * 盛裝Node元素的數組 它的大小是2的整數次冪 * Size is always a power of two. Accessed directly by iterators. */ transient volatile Node<K,V>[] table; /** * 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. hash表初始化或擴容時的一個控制位標識量。 負數表明正在進行初始化或擴容操做 -1表明正在初始化 -N 表示有N-1個線程正在進行擴容操做 正數或0表明hash表尚未被初始化,這個數值表示初始化或下一次進行擴容的大小 */ private transient volatile int sizeCtl; // 如下兩個是用來控制擴容的時候 單線程進入的變量 /** * 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 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; // hash值是-1,表示這是一個forwardNode節點 static final int TREEBIN = -2; // hash值是-2 表示這時一個TreeBin節點
Node是最核心的內部類,它包裝了key-value鍵值對,全部插入ConcurrentHashMap的數據都包裝在這裏面。它與HashMap中的定義很類似,可是可是有一些差異它對value和next屬性設置了volatile同步鎖(與JDK7的Segment相同),它不容許調用setValue方法直接改變Node的value域,它增長了find方法輔助map.get()方法。
樹節點類,另一個核心的數據結構。當鏈表長度過長的時候,會轉換爲TreeNode。可是與HashMap不相同的是,它並非直接轉換爲紅黑樹,而是把這些結點包裝成TreeNode放在TreeBin對象中,由TreeBin完成對紅黑樹的包裝。並且TreeNode在ConcurrentHashMap集成自Node類,而並不是HashMap中的集成自LinkedHashMap.Entry<K,V>類,也就是說TreeNode帶有next指針,這樣作的目的是方便基於TreeBin的訪問。
這個類並不負責包裝用戶的key、value信息,而是包裝的不少TreeNode節點。它代替了TreeNode的根節點,也就是說在實際的ConcurrentHashMap「數組」中,存放的是TreeBin對象,而不是TreeNode對象,這是與HashMap的區別。另外這個類還帶有了讀寫鎖。
這裏僅貼出它的構造方法。能夠看到在構造TreeBin節點時,僅僅指定了它的hash值爲TREEBIN常量,這也就是個標識爲。同時也看到咱們熟悉的紅黑樹構造方法
一個用於鏈接兩個table的節點類。它包含一個nextTable指針,用於指向下一張表。並且這個節點的key value next指針所有爲null,它的hash值爲-1. 這裏面定義的find的方法是從nextTable裏進行查詢節點,而不是以自身爲頭節點進行查找。
/** * 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); 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中,隨處能夠看到U, 大量使用了U.compareAndSwapXXX的方法,這個方法是利用一個CAS算法實現無鎖化的修改值的操做,他能夠大大下降鎖代理的性能消耗。這個算法的基本思想就是不斷地去比較當前內存中的變量值與你指定的一個變量值是否相等,若是相等,則接受你指定的修改的值,不然拒絕你的操做。由於當前線程中的值已經不是最新的值,你的修改極可能會覆蓋掉其餘線程修改的結果。這一點與樂觀鎖,SVN的思想是比較相似的。
unsafe代碼塊控制了一些屬性的修改工做,好比最經常使用的SIZECTL 。在這一版本的concurrentHashMap中,大量應用來的CAS方法進行變量、屬性的修改工做。利用CAS進行無鎖操做,能夠大大提升性能。
private static final sun.misc.Unsafe U; private static final long SIZECTL; private static final long TRANSFERINDEX; private static final long BASECOUNT; private static final long CELLSBUSY; private static final long CELLVALUE; private static final long ABASE; private static final int ASHIFT; static { try { U = sun.misc.Unsafe.getUnsafe(); Class<?> k = ConcurrentHashMap.class; SIZECTL = U.objectFieldOffset (k.getDeclaredField("sizeCtl")); TRANSFERINDEX = U.objectFieldOffset (k.getDeclaredField("transferIndex")); BASECOUNT = U.objectFieldOffset (k.getDeclaredField("baseCount")); CELLSBUSY = U.objectFieldOffset (k.getDeclaredField("cellsBusy")); Class<?> ck = CounterCell.class; CELLVALUE = U.objectFieldOffset (ck.getDeclaredField("value")); Class<?> ak = Node[].class; ABASE = U.arrayBaseOffset(ak); int scale = U.arrayIndexScale(ak); if ((scale & (scale - 1)) != 0) throw new Error("data type scale not a power of two"); ASHIFT = 31 - Integer.numberOfLeadingZeros(scale); } catch (Exception e) { throw new Error(e); } }
ConcurrentHashMap定義了三個原子操做,用於對指定位置的節點進行操做。正是這些原子操做保證了ConcurrentHashMap的線程安全。
//得到在i位置上的Node節點 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); } //利用CAS算法設置i位置上的Node節點。之因此能實現併發是由於他指定了原來這個節點的值是多少 //在CAS算法中,會比較內存中的值與你指定的這個值是否相等,若是相等才接受你的修改,不然拒絕你的修改 //所以當前線程中的值並非最新的值,這種修改可能會覆蓋掉其餘線程的修改結果 有點相似於SVN static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); } //利用volatile方法設置節點位置的值 static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) { U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v); }
對於ConcurrentHashMap來講,調用它的構造方法僅僅是設置了一些參數而已。而整個table的初始化是在向ConcurrentHashMap中插入元素的時候發生的。如調用put、computeIfAbsent、compute、merge等方法的時候,調用時機是檢查table==null。
初始化方法主要應用了關鍵屬性sizeCtl 若是這個值〈0,表示其餘線程正在進行初始化,就放棄這個操做。在這也能夠看出ConcurrentHashMap的初始化只能由一個線程完成。若是得到了初始化權限,就用CAS方法將sizeCtl置爲-1,防止其餘線程進入。初始化數組後,將sizeCtl的值改成0.75*n。
/** * 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表示有其餘線程正在進行初始化操做,把線程掛起。對於table的初始化工做,只能有一個線程在進行。 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//利用CAS方法把sizectl的值置爲-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 { sizeCtl = sc; } break; } } return tab; }
當ConcurrentHashMap容量不足的時候,須要對table進行擴容。這個方法的基本思想跟HashMap是很像的,可是因爲它是支持併發擴容的,因此要複雜的多。緣由是它支持多線程進行擴容操做,而並無加鎖。我想這樣作的目的不只僅是爲了知足concurrent的要求,而是但願利用併發處理去減小擴容帶來的時間影響。由於在擴容的時候,老是會涉及到從一個「數組」到另外一個「數組」拷貝的操做,若是這個操做可以併發進行,那真真是極好的了。
整個擴容操做分爲兩個部分
先來看一下單線程是如何完成的:
它的大致思想就是遍歷、複製的過程。首先根據運算獲得須要遍歷的次數i,而後利用tabAt方法得到i位置的元素:
再看一下多線程是如何完成的:
在代碼的69行有一個判斷,若是遍歷到的節點是forward節點,就向後繼續遍歷,再加上給節點上鎖的機制,就完成了多線程的控制。多線程遍歷節點,處理了一個節點,就把對應點的值set爲forward,另外一個線程看到forward,就向後遍歷。這樣交叉就完成了複製工做。並且還很好的解決了線程安全的問題。這個方法的設計實在是讓我膜拜。
/** * 一個過渡的table表 只有在擴容的時候纔會使用 */ private transient volatile Node<K,V>[] nextTable; /** * Moves and/or copies the nodes in each bin to new table. See * above for explanation. */ private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { int n = tab.length, stride; if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // subdivide range if (nextTab == null) { // initiating try { @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];//構造一個nextTable對象 它的容量是原來的兩倍 nextTab = nt; } catch (Throwable ex) { // try to cope with OOME sizeCtl = Integer.MAX_VALUE; return; } nextTable = nextTab; transferIndex = n; } int nextn = nextTab.length; ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);//構造一個連節點指針 用於標誌位 boolean advance = true;//併發擴容的關鍵屬性 若是等於true 說明這個節點已經處理過 boolean finishing = false; // to ensure sweep before committing nextTab for (int i = 0, bound = 0;;) { Node<K,V> f; int fh; //這個while循環體的做用就是在控制i-- 經過i--能夠依次遍歷原hash表中的節點 while (advance) { int nextIndex, nextBound; if (--i >= bound || finishing) advance = false; else if ((nextIndex = transferIndex) <= 0) { i = -1; advance = false; } else if (U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { bound = nextBound; i = nextIndex - 1; advance = false; } } if (i < 0 || i >= n || i + n >= nextn) { int sc; if (finishing) { //若是全部的節點都已經完成複製工做 就把nextTable賦值給table 清空臨時對象nextTable nextTable = null; table = nextTab; sizeCtl = (n << 1) - (n >>> 1);//擴容閾值設置爲原來容量的1.5倍 依然至關於如今容量的0.75倍 return; } //利用CAS方法更新這個擴容閾值,在這裏面sizectl值減一,說明新加入一個線程參與到擴容操做 if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return; finishing = advance = true; i = n; // recheck before commit } } //若是遍歷到的節點爲空 則放入ForwardingNode指針 else if ((f = tabAt(tab, i)) == null) advance = casTabAt(tab, i, null, fwd); //若是遍歷到ForwardingNode節點 說明這個點已經被處理過了 直接跳過 這裏是控制併發擴容的核心 else if ((fh = f.hash) == MOVED) advance = true; // already processed else { //節點上鎖 synchronized (f) { if (tabAt(tab, i) == f) { Node<K,V> ln, hn; //若是fh>=0 證實這是一個Node節點 if (fh >= 0) { int runBit = fh & n; //如下的部分在完成的工做是構造兩個鏈表 一個是原鏈表 另外一個是原鏈表的反序排列 Node<K,V> lastRun = f; for (Node<K,V> p = f.next; p != null; p = p.next) { int b = p.hash & n; if (b != runBit) { runBit = b; lastRun = p; } } if (runBit == 0) { ln = lastRun; hn = null; } else { hn = lastRun; ln = null; } for (Node<K,V> p = f; p != lastRun; p = p.next) { int ph = p.hash; K pk = p.key; V pv = p.val; if ((ph & n) == 0) ln = new Node<K,V>(ph, pk, pv, ln); else hn = new Node<K,V>(ph, pk, pv, hn); } //在nextTable的i位置上插入一個鏈表 setTabAt(nextTab, i, ln); //在nextTable的i+n的位置上插入另外一個鏈表 setTabAt(nextTab, i + n, hn); //在table的i位置上插入forwardNode節點 表示已經處理過該節點 setTabAt(tab, i, fwd); //設置advance爲true 返回到上面的while循環中 就能夠執行i--操做 advance = true; } //對TreeBin對象進行處理 與上面的過程相似 else if (f instanceof TreeBin) { TreeBin<K,V> t = (TreeBin<K,V>)f; TreeNode<K,V> lo = null, loTail = null; TreeNode<K,V> hi = null, hiTail = null; int lc = 0, hc = 0; //構造正序和反序兩個鏈表 for (Node<K,V> e = t.first; e != null; e = e.next) { int h = e.hash; TreeNode<K,V> p = new TreeNode<K,V> (h, e.key, e.val, null, null); if ((h & n) == 0) { if ((p.prev = loTail) == null) lo = p; else loTail.next = p; loTail = p; ++lc; } else { if ((p.prev = hiTail) == null) hi = p; else hiTail.next = p; hiTail = p; ++hc; } } //若是擴容後已經再也不須要tree的結構 反向轉換爲鏈表結構 ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0) ? new TreeBin<K,V>(lo) : t; hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0) ? new TreeBin<K,V>(hi) : t; //在nextTable的i位置上插入一個鏈表 setTabAt(nextTab, i, ln); //在nextTable的i+n的位置上插入另外一個鏈表 setTabAt(nextTab, i + n, hn); //在table的i位置上插入forwardNode節點 表示已經處理過該節點 setTabAt(tab, i, fwd); //設置advance爲true 返回到上面的while循環中 就能夠執行i--操做 advance = true; } } } } } }
前面的全部的介紹其實都爲這個方法作鋪墊。ConcurrentHashMap最經常使用的就是put和get兩個方法。如今來介紹put方法,這個put方法依然沿用HashMap的put方法的思想,根據hash值計算這個新插入的點在table中的位置i,若是i位置是空的,直接放進去,不然進行判斷,若是i位置是樹節點,按照樹的方式插入新的節點,不然把i插入到鏈表的末尾。ConcurrentHashMap中依然沿用這個思想,有一個最重要的不一樣點就是ConcurrentHashMap不容許key或value爲null值。另外因爲涉及到多線程,put方法就要複雜一點。在多線程中可能有如下兩個狀況
總體流程就是首先定義不容許key或value爲null的狀況放入 對於每個放入的值,首先利用spread方法對key的hashcode進行一次hash計算,由此來肯定這個值在table中的位置。
若是這個位置是空的,那麼直接放入,並且不須要加鎖操做。
若是這個位置存在結點,說明發生了hash碰撞,首先判斷這個節點的類型。若是是鏈表節點(fh>0),則獲得的結點就是hash值相同的節點組成的鏈表的頭節點。須要依次向後遍歷肯定這個新加入的值所在位置。若是遇到hash值與key值都與新加入節點是一致的狀況,則只須要更新value值便可。不然依次向後遍歷,直到鏈表尾插入這個結點。若是加入這個節點之後鏈表長度大於8,就把這個鏈表轉換成紅黑樹。若是這個節點的類型已是樹節點的話,直接調用樹節點的插入方法進行插入新的值。
public V put(K key, V value) { return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { //不容許 key或value爲null 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爲空的話,初始化table if (tab == null || (n = tab.length) == 0) tab = initTable(); //根據hash值計算出在table裏面的位置 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) tab = helpTransfer(tab, f); else { V oldVal = null; //結點上鎖 這裏的結點能夠理解爲hash值相同組成的鏈表的頭結點 synchronized (f) { if (tabAt(tab, i) == f) { //fh〉0 說明這個節點是一個鏈表的節點 不是樹的節點 if (fh >= 0) { binCount = 1; //在這裏遍歷鏈表全部的結點 for (Node<K,V> e = f;; ++binCount) { K ek; //若是hash值和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; //若是遍歷到了最後一個結點,那麼就證實新的節點須要插入 就把它插入在鏈表尾部 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) { //若是鏈表長度已經達到臨界值8 就須要把鏈表轉換爲樹結構 if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } //將當前ConcurrentHashMap的元素數量+1 addCount(1L, binCount); return null; }
咱們能夠發現JDK8中的實現也是鎖分離的思想,只是鎖住的是一個Node,而不是JDK7中的Segment,而鎖住Node以前的操做是無鎖的而且也是線程安全的,創建在以前提到的3個原子操做上。
這是一個協助擴容的方法。這個方法被調用的時候,當前ConcurrentHashMap必定已經有了nextTable對象,首先拿到這個nextTable對象,調用transfer方法。回看上面的transfer方法能夠看到,當本線程進入擴容方法的時候會直接進入複製階段。
這個方法用於將過長的鏈表轉換爲TreeBin對象。可是他並非直接轉換,而是進行一次容量判斷,若是容量沒有達到轉換的要求,直接進行擴容操做並返回;若是知足條件才鏈表的結構抓換爲TreeBin ,這與HashMap不一樣的是,它並無把TreeNode直接放入紅黑樹,而是利用了TreeBin這個小容器來封裝全部的TreeNode.
get方法比較簡單,給定一個key來肯定value的時候,必須知足兩個條件 key相同 hash值相同,對於節點可能在鏈表或樹上的狀況,須要分別去查找。
public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; //計算hash值 int h = spread(key.hashCode()); //根據hash值肯定節點位置 if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { //若是搜索到的節點key與傳入的key相同且不爲null,直接返回這個節點 if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } //若是eh<0 說明這個節點在樹上 直接尋找 else if (eh < 0) 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; }
對於ConcurrentHashMap來講,這個table裏到底裝了多少東西實際上是個不肯定的數量,由於不可能在調用size()方法的時候像GC的「stop the world」同樣讓其餘線程都停下來讓你去統計,所以只能說這個數量是個估計值。對於這個估計值,ConcurrentHashMap也是大費周章才計算出來的。
爲了統計元素個數,ConcurrentHashMap定義了一些變量和一個內部類
/** * A padded cell for distributing counts. Adapted from LongAdder * and Striped64. See their internal docs for explanation. */ @sun.misc.Contended static final class CounterCell { volatile long value; CounterCell(long x) { value = x; } } /******************************************/ /** * 實際上保存的是hashmap中的元素個數 利用CAS鎖進行更新 但它並不用返回當前hashmap的元素個數 */ private transient volatile long baseCount; /** * Spinlock (locked via CAS) used when resizing and/or creating CounterCells. */ private transient volatile int cellsBusy; /** * Table of counter cells. When non-null, size is a power of 2. */ private transient volatile CounterCell[] counterCells;
mappingCount與size方法的相似 從Java工程師給出的註釋來看,應該使用mappingCount代替size方法 兩個方法都沒有直接返回basecount 而是統計一次這個值,而這個值其實也是一個大概的數值,所以可能在統計的時候有其餘線程正在執行插入或刪除操做。
public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n); } /** * Returns the number of mappings. This method should be used * instead of {@link #size} because a ConcurrentHashMap may * contain more mappings than can be represented as an int. The * value returned is an estimate; the actual count may differ if * there are concurrent insertions or removals. * * @return the number of mappings * @since 1.8 */ public long mappingCount() { long n = sumCount(); return (n < 0L) ? 0L : n; // ignore transient negative values } final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value;//全部counter的值求和 } } return sum; }
在put方法結尾處調用了addCount方法,把當前ConcurrentHashMap的元素個數+1這個方法一共作了兩件事,更新baseCount的值,檢測是否進行擴容。
private final void addCount(long x, int check) { CounterCell[] as; long b, s; //利用CAS方法更新baseCount的值 if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || (a = as[ThreadLocalRandom.getProbe() & m]) == null || !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { fullAddCount(x, uncontended); return; } if (check <= 1) return; s = sumCount(); } //若是check值大於等於0 則須要檢驗是否須要進行擴容操做 if (check >= 0) { Node<K,V>[] tab, nt; int n, sc; while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { int rs = resizeStamp(n); // if (sc < 0) { if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; //若是已經有其餘線程在執行擴容操做 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt); } //當前線程是惟一的或是第一個發起擴容的線程 此時nextTable=null else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); s = sumCount(); } } }
JDK6,7中的ConcurrentHashmap主要使用Segment來實現減少鎖粒度,把HashMap分割成若干個Segment,在put的時候須要鎖住Segment,get時候不加鎖,使用volatile來保證可見性,當要統計全局時(好比size),首先會嘗試屢次計算modcount來肯定,這幾回嘗試中,是否有其餘線程進行了修改操做,若是沒有,則直接返回size。若是有,則須要依次鎖住全部的Segment來計算。
jdk7中ConcurrentHashmap中,當長度過長碰撞會很頻繁,鏈表的增改刪查操做都會消耗很長的時間,影響性能,因此jdk8 中徹底重寫了concurrentHashmap,代碼量從原來的1000多行變成了 6000多 行,實現上也和原來的分段式存儲有很大的區別。
主要設計上的變化有如下幾點:
至於爲何JDK8中使用synchronized而不是ReentrantLock,我猜是由於JDK8中對synchronized有了足夠的優化吧。
Reference:
歡迎關注公衆號 【碼農開花】一塊兒學習成長