集合框架(12)-----ConcurrentHashMap

目錄

1. ConcurrentHashMap的實現——JDK7版本

1.1 分段鎖機制

1.2 ConcurrentHashMap的數據結構

1.3 ConcurrentHashMap的初始化

1.3.1 初始化ConcurrentHashMap

1.3.2 初始化Segment分段

1.4 定位Segment

1.5 ConcurrentHashMap的操作

1.5.1 get

1.5.2 put

1.5.3 size

2. ConcurrentHashMap的實現——JDK8版本

2.1 CAS原理

2.2 ConcurrentHashMap的數據結構

2.3 ConcurrentHashMap的初始化

2.4 Node鏈表和紅黑樹結構轉換

2.5 ConcurrentHashMap的操作

2.5.1 get

2.5.2 put

2.5.3 size


ConcurrentHashMap從JDK1.5開始隨java.util.concurrent包一起引入JDK中,主要爲了解決HashMap線程不安全和Hashtable效率不高的問題。衆所周知,HashMap在多線程編程中是線程不安全的,而Hashtable由於使用了synchronized修飾方法而導致執行效率不高;因此,在concurrent包中,實現了ConcurrentHashMap以使在多線程編程中可以使用一個高性能的線程安全HashMap方案。

而JDK1.7之前的ConcurrentHashMap使用分段鎖機制實現,JDK1.8則使用數組+鏈表+紅黑樹數據結構和CAS原子操作實現ConcurrentHashMap;本文將分別介紹這兩種方式的實現方案及其區別。

1. ConcurrentHashMap的實現——JDK7版本

1.1 分段鎖機制

Hashtable之所以效率低下主要是因爲其實現使用了synchronized關鍵字對put等操作進行加鎖,而synchronized關鍵字加鎖是對整個對象進行加鎖,也就是說在進行put等修改Hash表的操作時,鎖住了整個Hash表,從而使得其表現的效率低下;因此,在JDK1.5~1.7版本,Java使用了分段鎖機制實現ConcurrentHashMap.

簡而言之,ConcurrentHashMap在對象中保存了一個Segment數組,即將整個Hash表劃分爲多個分段;而每個Segment元素,即每個分段則類似於一個Hashtable;這樣,在執行put操作時首先根據hash算法定位到元素屬於哪個Segment,然後對該Segment加鎖即可。因此,ConcurrentHashMap在多線程併發編程中可是實現多線程put操作。接下來,本文將詳細分析JDK1.7版本中ConcurrentHashMap的實現原理。

1.2 ConcurrentHashMap的數據結構

ConcurrentHashMap類結構如上圖所示。由圖可知,在ConcurrentHashMap中,定義了一個Segment<K, V>[]數組來將Hash表實現分段存儲,從而實現分段加鎖;而麼一個Segment元素則與HashMap結構類似,其包含了一個HashEntry數組,用來存儲Key/Value對。Segment繼承了ReetrantLock,表示Segment是一個可重入鎖,因此ConcurrentHashMap通過可重入鎖對每個分段進行加鎖。

1.3 ConcurrentHashMap的初始化

JDK1.7的ConcurrentHashMap的初始化主要分爲兩個部分:一是初始化ConcurrentHashMap,即初始化segments數組、segmentShift段偏移量和segmentMask段掩碼等;然後則是初始化每個segment分段。接下來,我們將分別介紹這兩部分初始化。

ConcurrentHashMap包含多個構造函數,而所有的構造函數最終都調用瞭如下的構造函數:

  1. public ConcurrentHashMap(int initialCapacity,
  2. float loadFactor, int concurrencyLevel) {
  3. if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
  4. throw new IllegalArgumentException();
  5. if (concurrencyLevel > MAX_SEGMENTS)
  6. concurrencyLevel = MAX_SEGMENTS;
  7. // Find power-of-two sizes best matching arguments
  8. int sshift = 0;
  9. int ssize = 1;
  10. while (ssize < concurrencyLevel) {
  11. ++sshift;
  12. ssize <<= 1;
  13. }
  14. this.segmentShift = 32 - sshift;
  15. this.segmentMask = ssize - 1;
  16. if (initialCapacity > MAXIMUM_CAPACITY)
  17. initialCapacity = MAXIMUM_CAPACITY;
  18. int c = initialCapacity / ssize;
  19. if (c * ssize < initialCapacity)
  20. ++c;
  21. int cap = MIN_SEGMENT_TABLE_CAPACITY;
  22. while (cap < c)
  23. cap <<= 1;
  24. // create segments and segments[0]
  25. Segment<K,V> s0 =
  26. new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
  27. (HashEntry<K,V>[])new HashEntry[cap]);
  28. Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
  29. UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
  30. this.segments = ss;
  31. }

由代碼可知,該構造函數需要傳入三個參數:initialCapacity、loadFactor、concurrencyLevel,其中,concurrencyLevel主要用來初始化segments、segmentShift和segmentMask等;而initialCapacity和loadFactor則主要用來初始化每個Segment分段。

1.3.1 初始化ConcurrentHashMap

根據ConcurrentHashMap的構造方法可知,在初始化時創建了兩個中間變量ssize和sshift,它們都是通過concurrencyLevel計算得到的。其中ssize表示了segments數組的長度,爲了能通過按位與的散列算法來定位segments數組的索引,必須保證segments數組的長度是2的N次方,所以在初始化時通過循環計算出一個大於或等於concurrencyLevel的最小的2的N次方值來作爲數組的長度;而sshift表示了計算ssize時進行移位操作的次數。

segmentShift用於定位參與散列運算的位數,其等於32減去sshift,使用32是因爲ConcurrentHashMap的hash()方法返回的最大數是32位的;segmentMask是散列運算的掩碼,等於ssize減去1,所以掩碼的二進制各位都爲1.

因爲ssize的最大長度爲65536,所以segmentShift最大值爲16,segmentMask最大值爲65535. 由於segmentShift和segmentMask與散列運算相關,因此之後還會對此進行分析。

1.3.2 初始化Segment分段

ConcurrentHashMap通過initialCapacity和loadFactor來初始化每個Segment. 在初始化Segment時,也定義了一箇中間變量cap,其等於initialCapacity除以ssize的倍數c,如果c大於1,則取大於等於c的2的N次方,cap表示Segment中HashEntry數組的長度;loadFactor表示了Segment的加載因子,通過cap*loadFactor獲得每個Segment的閾值threshold.

默認情況下,initialCapacity等於16,loadFactor等於0.75,concurrencyLevel等於16.

1.4 定位Segment

由於採用了Segment分段鎖機制實現一個高效的同步,那麼首先則需要通過hash散列算法計算key的hash值,從而定位其所在的Segment. 因此,首先需要了解ConcurrentHashMap中hash()函數的實現。

  1. private int hash(Object k) {
  2. int h = hashSeed;
  3. if ((0 != h) && (k instanceof String)) {
  4. return sun.misc.Hashing.stringHash32((String) k);
  5. }
  6. h ^= k.hashCode();
  7. // Spread bits to regularize both segment and index locations,
  8. // using variant of single-word Wang/Jenkins hash.
  9. h += (h << 15) ^ 0xffffcd7d;
  10. h ^= (h >>> 10);
  11. h += (h << 3);
  12. h ^= (h >>> 6);
  13. h += (h << 2) + (h << 14);
  14. return h ^ (h >>> 16);
  15. }

通過hash()函數可知,首先通過計算一個隨機的hashSeed減少String類型的key值的hash衝突;然後利用Wang/Jenkins hash算法對key的hash值進行再hash計算。通過這兩種方式都是爲了減少散列衝突,從而提高效率。因爲如果散列的質量太差,元素分佈不均,那麼使用Segment分段加鎖也就沒有意義了。

  1. private Segment<K,V> segmentForHash(int h) {
  2. long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
  3. return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u);
  4. }

接下來,ConcurrentHashMap通過上述定位函數則可以定位到key所在的Segment分段。

1.5 ConcurrentHashMap的操作

在介紹ConcurrentHashMap的操作之前,首先需要介紹一下Unsafe類,因爲在JDK1.7新版本中是通過Unsafe類的方法實現鎖操作的。Unsafe類是一個保護類,一般應用程序很少用到,但其在一些框架中經常用到,如JDK、Netty、Spring等框架。Unsafe類提供了一些硬件級別的原子操作,其在JDK1.7和JDK1.8中的ConcurrentHashMap都有用到,但其用法卻不同,在此只介紹在JDK1.7中用到的幾個方法:

  • arrayBaseOffset(Class class):獲取數組第一個元素的偏移地址。
  • arrayIndexScale(Class class):獲取數組中元素的增量地址。
  • getObjectVolatile(Object obj, long offset):獲取obj對象中offset偏移地址對應的Object型field屬性值,支持Volatile讀內存語義。

1.5.1 get

JDK1.7的ConcurrentHashMap的get操作是不加鎖的,因爲在每個Segment中定義的HashEntry數組和在每個HashEntry中定義的value和next HashEntry節點都是volatile類型的,volatile類型的變量可以保證其在多線程之間的可見性,因此可以被多個線程同時讀,從而不用加鎖。而其get操作步驟也比較簡單,定位Segment –> 定位HashEntry –> 通過getObjectVolatile()方法獲取指定偏移量上的HashEntry –> 通過循環遍歷鏈表獲取對應值。

定位Segment:(((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE

定位HashEntry:(((tab.length - 1) & h)) << TSHIFT) + TBASE

1.5.2 put

ConcurrentHashMap的put方法就要比get方法複雜的多,其實現源碼如下:

  1. public V put(K key, V value) {
  2. Segment<K,V> s;
  3. if (value == null)
  4. throw new NullPointerException();
  5. int hash = hash(key);
  6. int j = (hash >>> segmentShift) & segmentMask;
  7. if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
  8. (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
  9. s = ensureSegment(j);
  10. return s.put(key, hash, value, false);
  11. }

同樣的,put方法首先也會通過hash算法定位到對應的Segment,此時,如果獲取到的Segment爲空,則調用ensureSegment()方法;否則,直接調用查詢到的Segment的put方法插入值,注意此處並沒有用getObjectVolatile()方法讀,而是在ensureSegment()中再用volatile讀操作,這樣可以在查詢segments不爲空的時候避免使用volatile讀,提高效率。在ensureSegment()方法中,首先使用getObjectVolatile()讀取對應Segment,如果還是爲空,則以segments[0]爲原型創建一個Segment對象,並將這個對象設置爲對應的Segment值並返回。

在Segment的put方法中,首先需要調用tryLock()方法獲取鎖,然後通過hash算法定位到對應的HashEntry,然後遍歷整個鏈表,如果查到key值,則直接插入元素即可;而如果沒有查詢到對應的key,則需要調用rehash()方法對Segment中保存的table進行擴容,擴容爲原來的2倍,並在擴容之後插入對應的元素。插入一個key/value對後,需要將統計Segment中元素個數的count屬性加1。最後,插入成功之後,需要使用unLock()釋放鎖。

1.5.3 size

ConcurrentHashMap的size操作的實現方法也非常巧妙,一開始並不對Segment加鎖,而是直接嘗試將所有的Segment元素中的count相加,這樣執行兩次,然後將兩次的結果對比,如果兩次結果相等則直接返回;而如果兩次結果不同,則再將所有Segment加鎖,然後再執行統計得到對應的size值。

2. ConcurrentHashMap的實現——JDK8版本

在JDK1.7之前,ConcurrentHashMap是通過分段鎖機制來實現的,所以其最大併發度受Segment的個數限制。因此,在JDK1.8中,ConcurrentHashMap的實現原理摒棄了這種設計,而是選擇了與HashMap類似的數組+鏈表+紅黑樹的方式實現,而加鎖則採用CAS和synchronized實現。

2.1 CAS原理

一般地,鎖分爲悲觀鎖和樂觀鎖:悲觀鎖認爲對於同一個數據的併發操作,一定是爲發生修改的;而樂觀鎖則任務對於同一個數據的併發操作是不會發生修改的,在更新數據時會採用嘗試更新不斷重試的方式更新數據。

CAS(Compare And Swap,比較交換):CAS有三個操作數,內存值V、預期值A、要修改的新值B,當且僅當A和V相等時纔會將V修改爲B,否則什麼都不做。Java中CAS操作通過JNI本地方法實現,在JVM中程序會根據當前處理器的類型來決定是否爲cmpxchg指令添加lock前綴。如果程序是在多處理器上運行,就爲cmpxchg指令加上lock前綴(Lock Cmpxchg);反之,如果程序是在單處理器上運行,就省略lock前綴。

Intel的手冊對lock前綴的說明如下:

  1. 確保對內存的讀-改-寫操作原子執行。之前採用鎖定總線的方式,但開銷很大;後來改用緩存鎖定來保證指令執行的原子性。
  2. 禁止該指令與之前和之後的讀和寫指令重排序。
  3. 把寫緩衝區中的所有數據刷新到內存中。

CAS同時具有volatile讀和volatile寫的內存語義。

不過CAS操作也存在一些缺點:1. 存在ABA問題,其解決思路是使用版本號;2. 循環時間長,開銷大;3. 只能保證一個共享變量的原子操作。

爲了能更好的利用CAS原理解決併發問題,JDK1.5之後在java.util.concurrent.atomic包下采用CAS實現了一系列的原子操作類,這在之後的文章中會詳細分析介紹。

2.2 ConcurrentHashMap的數據結構

JDK1.8的ConcurrentHashMap數據結構比JDK1.7之前的要簡單的多,其使用的是HashMap一樣的數據結構:數組+鏈表+紅黑樹。ConcurrentHashMap中包含一個table數組,其類型是一個Node數組;而Node是一個繼承自Map.Entry<K, V>的鏈表,而當這個鏈表結構中的數據大於8,則將數據結構升級爲TreeBin類型的紅黑樹結構。另外,JDK1.8中的ConcurrentHashMap中還包含一個重要屬性sizeCtl,其是一個控制標識符,不同的值代表不同的意思:其爲0時,表示hash表還未初始化,而爲正數時這個數值表示初始化或下一次擴容的大小,相當於一個閾值;即如果hash表的實際大小>=sizeCtl,則進行擴容,默認情況下其是當前ConcurrentHashMap容量的0.75倍;而如果sizeCtl爲-1,表示正在進行初始化操作;而爲-N時,則表示有N-1個線程正在進行擴容。

2.3 ConcurrentHashMap的初始化

JDK1.8的ConcurrentHashMap的初始化過程也比較簡單,所有的構造方法最終都會調用如下這個構造方法。

  1. public ConcurrentHashMap(int initialCapacity,
  2. float loadFactor, int concurrencyLevel) {
  3. if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
  4. throw new IllegalArgumentException();
  5. if (initialCapacity < concurrencyLevel) // Use at least as many bins
  6. initialCapacity = concurrencyLevel; // as estimated threads
  7. long size = (long)(1.0 + (long)initialCapacity / loadFactor);
  8. int cap = (size >= (long)MAXIMUM_CAPACITY) ?
  9. MAXIMUM_CAPACITY : tableSizeFor((int)size);
  10. this.sizeCtl = cap;
  11. }

該初始化過程通過指定的初始容量initialCapacity,加載因子loadFactor和預估併發度concurrencyLevel三個參數計算table數組的初始大小sizeCtl的值。

可以看到,在構造ConcurrentHashMap時,並不會對hash表(Node<K, V>[] table)進行初始化,hash表的初始化是在插入第一個元素時進行的。在put操作時,如果檢測到table爲空或其長度爲0時,則會調用initTable()方法對table進行初始化操作。

  1. private final Node<K,V>[] initTable() {
  2. Node<K,V>[] tab; int sc;
  3. while ((tab = table) == null || tab.length == 0) {
  4. if ((sc = sizeCtl) < 0)
  5. Thread.yield(); // lost initialization race; just spin
  6. else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
  7. try {
  8. if ((tab = table) == null || tab.length == 0) {
  9. int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
  10. @SuppressWarnings("unchecked")
  11. Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
  12. table = tab = nt;
  13. sc = n - (n >>> 2);
  14. }
  15. } finally {
  16. sizeCtl = sc;
  17. }
  18. break;
  19. }
  20. }
  21. return tab;
  22. }

可以看到,該方法使用一個循環實現table的初始化;在循環中,首先會判斷sizeCtl的值,如果其小於0,則說明其正在進行初始化或擴容操作,則不執行任何操作,調用yield()方法使當前線程返回等待狀態;而如果sizeCtl大於等於0,則使用CAS操作比較sizeCtl的值是否是-1,如果是-1則進行初始化。初始化時,如果sizeCtl的值爲0,則創建默認容量的table;否則創建大小爲sizeCtl的table;然後重置sizeCtl的值爲0.75n,即當前table容量的0.75倍,並返回創建的table,此時初始化hash表完成。

2.4 Node鏈表和紅黑樹結構轉換

上文中說到,一個table元素會根據其包含的Node節點數在鏈表和紅黑樹兩種結構之間切換,因此我們本節先介紹Node節點的結構轉換的實現。

首先,在table中添加一個元素時,如果添加元素的鏈表節點個數超過8,則會觸發鏈表向紅黑樹結構轉換。具體的實現方法如下:

  1. private final void treeifyBin(Node<K,V>[] tab, int index) {
  2. Node<K,V> b; int n, sc;
  3. if (tab != null) {
  4. if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
  5. tryPresize(n << 1);
  6. else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
  7. synchronized (b) {
  8. if (tabAt(tab, index) == b) {
  9. TreeNode<K,V> hd = null, tl = null;
  10. for (Node<K,V> e = b; e != null; e = e.next) {
  11. TreeNode<K,V> p =
  12. new TreeNode<K,V>(e.hash, e.key, e.val,
  13. null, null);
  14. if ((p.prev = tl) == null)
  15. hd = p;
  16. else
  17. tl.next = p;
  18. tl = p;
  19. }
  20. setTabAt(tab, index, new TreeBin<K,V>(hd));
  21. }
  22. }
  23. }
  24. }
  25. }

該方法首先會檢查hash表的大小是否大於等於MIN_TREEIFY_CAPACITY,默認值爲64,如果小於該值,則表示不需要轉化爲紅黑樹結構,直接將hash表擴容即可。

如果當前table的長度大於64,則使用CAS獲取指定的Node節點,然後對該節點通過synchronized加鎖,由於只對一個Node節點加鎖,因此該操作並不影響其他Node節點的操作,因此極大的提高了ConcurrentHashMap的併發效率。加鎖之後,便是將這個Node節點所在的鏈表轉換爲TreeBin結構的紅黑樹。

然後,在table中刪除元素時,如果元素所在的紅黑樹節點個數小於6,則會觸發紅黑樹向鏈表結構轉換。具體實現如下:

  1. static <K,V> Node<K,V> untreeify(Node<K,V> b) {
  2. Node<K,V> hd = null, tl = null;
  3. for (Node<K,V> q = b; q != null; q = q.next) {
  4. Node<K,V> p = new Node<K,V>(q.hash, q.key, q.val, null);
  5. if (tl == null)
  6. hd = p;
  7. else
  8. tl.next = p;
  9. tl = p;
  10. }
  11. return hd;
  12. }

該方法實現簡單,在此不再進行細緻分析。

2.5 ConcurrentHashMap的操作

2.5.1 get

通過get獲取hash表中的值時,首先需要獲取key值的hash值。而在JDK1.8的ConcurrentHashMap中通過speed()方法獲取。

  1. static final int spread(int h) {
  2. return (h ^ (h >>> 16)) & HASH_BITS;
  3. }

speed()方法將key的hash值進行再hash,讓hash值的高位也參與hash運算,從而減少哈希衝突。然後再查詢對應的value值。

  1. public V get(Object key) {
  2. Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
  3. int h = spread(key.hashCode());
  4. if ((tab = table) != null && (n = tab.length) > 0 &&
  5. (e = tabAt(tab, (n - 1) & h)) != null) {
  6. if ((eh = e.hash) == h) {
  7. if ((ek = e.key) == key || (ek != null && key.equals(ek)))
  8. return e.val;
  9. }
  10. else if (eh < 0)
  11. return (p = e.find(h, key)) != null ? p.val : null;
  12. while ((e = e.next) != null) {
  13. if (e.hash == h &&
  14. ((ek = e.key) == key || (ek != null && key.equals(ek))))
  15. return e.val;
  16. }
  17. }
  18. return null;
  19. }

查詢時,首先通過tabAt()方法找到key對應的Node鏈表或紅黑樹,然後遍歷該結構便可以獲取key對應的value值。其中,tabAt()方法主要通過Unsafe類的getObjectVolatile()方法獲取value值,通過volatile讀獲取value值,可以保證value值的可見性,從而保證其是當前最新的值。

2.5.2 put

JDK1.8的ConcurrentHashMap的put操作實現方式主要定義在putVal(K key, V value, boolean onlyIfAbsent)中。

 

  1. final V putVal(K key, V value, boolean onlyIfAbsent) {
  2. if (key == null || value == null) throw new NullPointerException();
  3. int hash = spread(key.hashCode());
  4. int binCount = 0;
  5. for (Node<K,V>[] tab = table;;) {
  6. Node<K,V> f; int n, i, fh;
  7. if (tab == null || (n = tab.length) == 0)
  8. tab = initTable();
  9. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
  10. if (casTabAt(tab, i, null,
  11. new Node<K,V>(hash, key, value, null)))
  12. break; // no lock when adding to empty bin
  13. }
  14. else if ((fh = f.hash) == MOVED)
  15. tab = helpTransfer(tab, f);
  16. else {
  17. V oldVal = null;
  18. synchronized (f) {
  19. if (tabAt(tab, i) == f) {
  20. if (fh >= 0) {
  21. binCount = 1;
  22. for (Node<K,V> e = f;; ++binCount) {
  23. K ek;
  24. if (e.hash == hash &&
  25. ((ek = e.key) == key ||
  26. (ek != null && key.equals(ek)))) {
  27. oldVal = e.val;
  28. if (!onlyIfAbsent)
  29. e.val = value;
  30. break;
  31. }
  32. Node<K,V> pred = e;
  33. if ((e = e.next) == null) {
  34. pred.next = new Node<K,V>(hash, key,
  35. value, null);
  36. break;
  37. }
  38. }
  39. }
  40. else if (f instanceof TreeBin) {
  41. Node<K,V> p;
  42. binCount = 2;
  43. if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
  44. value)) != null) {
  45. oldVal = p.val;
  46. if (!onlyIfAbsent)
  47. p.val = value;
  48. }
  49. }
  50. }
  51. }
  52. if (binCount != 0) {
  53. if (binCount >= TREEIFY_THRESHOLD)
  54. treeifyBin(tab, i);
  55. if (oldVal != null)
  56. return oldVal;
  57. break;
  58. }
  59. }
  60. }
  61. addCount(1L, binCount);
  62. return null;
  63. }

put操作大致可分爲以下幾個步驟:

  • 計算key的hash值,即調用speed()方法計算hash值;
  • 獲取hash值對應的Node節點位置,此時通過一個循環實現。有以下幾種情況:
  1. 如果table表爲空,則首先進行初始化操作,初始化之後再次進入循環獲取Node節點的位置;
  2. 如果table不爲空,但沒有找到key對應的Node節點,則直接調用casTabAt()方法插入一個新節點,此時不用加鎖;
  3. 如果table不爲空,且key對應的Node節點也不爲空,但Node頭結點的hash值爲MOVED(-1),則表示需要擴容,此時調用helpTransfer()方法進行擴容;
  4. 其他情況下,則直接向Node中插入一個新Node節點,此時需要對這個Node鏈表或紅黑樹通過synchronized加鎖。
  • 插入元素後,判斷對應的Node結構是否需要改變結構,如果需要則調用treeifyBin()方法將Node鏈表升級爲紅黑樹結構;
  • 最後,調用addCount()方法記錄table中元素的數量。

2.5.3 size

JDK1.8的ConcurrentHashMap中保存元素的個數的記錄方法也有不同,首先在添加和刪除元素時,會通過CAS操作更新ConcurrentHashMap的baseCount屬性值來統計元素個數。但是CAS操作可能會失敗,因此,ConcurrentHashMap又定義了一個CounterCell數組來記錄CAS操作失敗時的元素個數。因此,ConcurrentHashMap中元素的個數則通過如下方式獲得:

元素總數 = baseCount + sum(CounterCell)

  1. final long sumCount() {
  2. CounterCell[] as = counterCells; CounterCell a;
  3. long sum = baseCount;
  4. if (as != null) {
  5. for (int i = 0; i < as.length; ++i) {
  6. if ((a = as[i]) != null)
  7. sum += a.value;
  8. }
  9. }
  10. return sum;
  11. }

而JDK1.8中提供了兩種方法獲取ConcurrentHashMap中的元素個數。

  1. public int size() {
  2. long n = sumCount();
  3. return ((n < 0L) ? 0 :
  4. (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
  5. (int)n);
  6. }
  7. public long mappingCount() {
  8. long n = sumCount();
  9. return (n < 0L) ? 0L : n; // ignore transient negative values
  10. }

如代碼所示,size只能獲取int範圍內的ConcurrentHashMap元素個數;而如果hash表中的數據過多,超過了int類型的最大值,則推薦使用mappingCount()方法獲取其元素個數。

以上主要分析了ConcurrentHashMap在JDK1.7和JDK1.8中的兩種不同實現方案,當然ConcurrentHashMap的功能強大,還有很多方法本文都未能詳細解析,但其分析方法與本文以上的內容類似,因此不再贅述,感興趣的同學可以自行分析比較。通過學習JDK源碼,對以後的Java程序設計也有一定的幫助。本系列文章將深入剖析Java concurrent包中的併發編程設計,並從中提煉出一些使用場景,從而爲今後的Java程序設計提供一些小小的靈感。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章