說明
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操作: