ConcurrentHashMap源碼解讀

說明

JDK版本jdk1.8.0_191 1

建議先理解HashMap的源碼,再來理解ConcurrentHashMap源碼
HashMap源碼解讀: HashMap源碼解讀

1. 核心數據結構

首先了解一下繼承體系:

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable
public abstract class AbstractMap<K,V> implements Map<K,V>
public interface ConcurrentMap<K, V> extends Map<K, V>

再看看內部數據結構表示;與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;
/**
* Key-value entry.  This class is never exported out as a
* user-mutable Map.Entry (i.e., one supporting setValue; see
* MapEntry below), but can be used for read-only traversals used
* in bulk tasks.  Subclasses of Node with a negative hash field
* are special, and contain null keys and values (but are never
* exported).  Otherwise, keys and vals are never null.
*/
static class Node<K,V> implements Map.Entry<K,V> {
   final int hash;
   final K key;
   volatile V val;
   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; }
   public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
   public final String toString(){ return key + "=" + val; }
   public final V setValue(V value) {
       throw new UnsupportedOperationException();
   }

   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.
    */
   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的數據結構大同小異,要注意的是volatile關鍵字

還有一個數據結構,就是分段鎖Segment。簡單講,就是降低鎖的細粒度,將鎖的範圍縮小到表的桶級別的大小 可以看到Segment的註釋,它是早期的ConcurrentHashMap併發解決方案,所以這裏我們只簡單介紹一下Segment
在這裏插入圖片描述

/**
* Stripped-down version of helper class used in previous version,
* declared for the sake of serialization compatibility
*/
static class Segment<K,V> extends ReentrantLock implements Serializable {
   private static final long serialVersionUID = 2249069246763182397L;
   final float loadFactor;
   Segment(float lf) { this.loadFactor = lf; }
}

對於 當前ConcurrentHashMap版本(JDK1.8)來說,它並沒有使用Segment分段鎖。或者說沒必要使用分段鎖,它使用的是CAS + Synchronized,能讓鎖更加地細粒化。Synchronized只鎖定table表桶節點的鏈表首節點或者紅黑樹的根節點。
在這裏插入圖片描述

下面還有一個字段是非常重要的sizeCtl: 表初始化和resize控制使用的。當爲負數時,表正在被初始化/擴容,-1代表初始化,-(1+n) 代表有n個活躍地參與擴容的線程。否則,table爲null時,sizeCtl表示表要創建時的初始大小。在初始化之後,sizeCtl表示一旦擴容,下一個要擴容的大小

sizeCtl是volatile修飾的,所以用來作爲一個控制標識符很合適

/**
* 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.
*/
private transient volatile int sizeCtl;

2. ConcurrentHashMap#size

首先研究一下size方法:

public int size() {
  long n = sumCount();
  return ((n < 0L) ? 0 :
          (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
          (int)n);
}

size()是通過sumCount()函數來計算的。

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;
        }
    }
    return sum;
}

這裏先看到counterCells,計數器表。所以size()方法的計算就是通過CounterCell[] counterCells的累加和來計算的。

/**
* Table of counter cells. When non-null, size is a power of 2.
*/
private transient volatile CounterCell[] counterCells;

3. ConcurrentHashMap#put

/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*
* <p>The value can be retrieved by calling the {@code get} method
* with a key that is equal to the original key.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with {@code key}, or
*         {@code null} if there was no mapping for {@code key}
* @throws NullPointerException if the specified key or value is null
*/
public V put(K key, V value) {
   return putVal(key, value, false);
}

putVal爲核心算法實現。

final V putVal(K key, V value, boolean onlyIfAbsent) {
  // 從這裏我們知道,ConcurrentHashMap不支持插入空的鍵或值
  // 而HashMap可以插入鍵或者值爲空的節點
  if (key == null || value == null) throw new NullPointerException();
  
  // 這裏使用的計算hash的”擾動函數“爲spread
  int hash = spread(key.hashCode());
  int binCount = 0;
  for (Node<K,V>[] tab = table;;) {
      Node<K,V> f; int n, i, fh;
      // table仍然是延遲初始化,這裏使用initTable進行初始化
      if (tab == null || (n = tab.length) == 0)
          tab = initTable();
      // 找到f,即桶的頭節點
      else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
          // 如果hash所確定的桶槽位仍然爲空(沒被佔用過)
          // 則使用CAS的方式填充key-value鍵值對到該槽位
          // CAS下面會詳細討論
          if (casTabAt(tab, i, null,
                       new Node<K,V>(hash, key, value, null)))
              break;                   // no lock when adding to empty bin
      }
      // 由於ConcurrentHashMap常處於併發場景下
      // 很可能其它線程正在擴容元素
      // 那麼我們就協助其擴容
      else if ((fh = f.hash) == MOVED)
          tab = helpTransfer(tab, f);
      else {
          // 
          V oldVal = null;
          // 獲取頭節點的鎖
          synchronized (f) {
              // 如果待插入的節點是鏈表的頭節點
              if (tabAt(tab, i) == f) {
                  // 如果hash值>=0
                  if (fh >= 0) {
                      binCount = 1;
                      // 遍歷鏈表,添加或者替換節點值
                      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)
                                  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) {
              // 如果節點數大於閾值,則鏈表轉化爲紅黑樹
              if (binCount >= TREEIFY_THRESHOLD)
                  treeifyBin(tab, i);
              if (oldVal != null)
                  return oldVal;
              break;
          }
      }
  }
  // 遞增計數器,這裏遞增的是CounterCell[]裏的元素
  addCount(1L, binCount);
  return null;
}

下面梳理一下putVal操作:
在這裏插入圖片描述

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