前言
ConcurrentHashMap是線程安全的HashMap,本篇將和大家一起來分析它在JDK1.8源碼(在JDK1.6、JDK1.7、JDK1.8中,每個版本的源碼都不同)。
1、簡介
ConcurrentHashMap內部數據結構是:數組+鏈表+紅黑樹。數組中存放元素地址的空間,稱之爲bucket。每個bucket存放的是一個Node節點,下面是它的內部類,實現了Map.Entry<K,V>接口:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
//以下省略部分代碼……
}
2、構造方法
/**
* 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表的初始化和調整大小的控制標誌。
* 小於0表示Hash表正在初始化或者擴容;
* -1表示正在初始化,-(1+N)表示有N個線程在進行擴容
* 否則,當表爲null時,保存創建時使用的初始化大小或者默認0;
* 初始化後,保存的是下一次擴容的大小。
*/
private transient volatile int sizeCtl;
//默認構造方法
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));
//sizeCtl爲initialCapacity的兩倍
this.sizeCtl = cap;
}
//將一個map轉換爲ConcurrentHashMap
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;
}
3、put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
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());
//bucket中保存的Node節點的長度
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果數組爲空或length長度爲0,則初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果要插入的元素所在的bucket中還沒有元素
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//插入Node到當前bucket
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
//退出當前死循環
break; // no lock when adding to empty bin
}
//如果hash值爲-1,則表示table數組正在擴容,則加入幫忙擴容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//使用synchronized對當前節點加鎖(鎖住的是一個bucket,體現了分段鎖思想)
synchronized (f) {
//再次檢測數組中當前節點是否更改,如更改,則退出當前循環再來走一遍
if (tabAt(tab, i) == f) {
//當前hash值大於0,則證明不是擴容,也不是紅黑樹,是鏈表
if (fh >= 0) {
//設置鏈表的節點數爲1
binCount = 1;
//遍歷整個鏈表
for (Node<K,V> e = f;; ++binCount) {
K ek;
//插入的key在其中存在(hash相等、equals也相等)
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
//替換當前key的value
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;
//節點個數設置爲2
binCount = 2;
//調用紅黑樹的插入方法
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {
//如果找到了這個元素,則賦值了新值(onlyIfAbsent默認false)
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//如果binCount不爲0,說明成功插入了元素或者替換了元素
if (binCount != 0) {
// 如果鏈表元素個數達到了8,則嘗試樹化(樹的binCount設置了爲2,不會再次樹化)
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
// 如果要插入的元素已經存在
if (oldVal != null)
//返回舊值,退出循環
return oldVal;
break;
}
}
}
//插入新值,元素個數加1(再次檢查是否要擴容)
addCount(1L, binCount);
//成功則返回null
return null;
}
整體流程如下:
- 檢查key_value是否爲空,任意爲null,則拋出異常;
- 檢查數組是否初始化,如果否,則進行初始化;
- 檢查數組中當前元素對應的bucket有無Node節點,如果無,則嘗試插入當前元素作爲第一個Node到對應的bucket中;
- 如果正在擴容,則加入進去幫忙擴容;
- 如果數組當前bucket中有Node節點,且沒有在擴容,則鎖住當前bucket;
- 如果當前元素對應的bucket存儲的是鏈表,則在鏈表中進行更新或插入;
- 如果當前元素對應的bucket存儲的是紅黑樹,則在紅黑樹中進行更新或插入;
- 如果元素存在,返回舊值;
- 如果元素不存在,則返回null,將map的元素個數加1,並檢查是否需要再次擴容。
3.1、初始化數組
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//如果sizeCtl<0說明正在初始化或者擴容
if ((sc = sizeCtl) < 0)
//讓出cpu
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 如果把sizeCtl原子更新爲-1成功,則當前線程進入初始化
try {
// 再次檢查table是否爲空,防止ABA問題
if ((tab = table) == null || tab.length == 0) {
//默認容量爲16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 設置sc爲數組長度的0.75倍(寫死了)
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
3.2、協助擴容
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 如果數組不爲空,並且當前bucket第一個元素爲ForwardingNode類型(表示當前bucket已經遷移完)
// 擴容時會把舊bucket的第一個元素置爲ForwardingNode,並讓其nextTab指向新bucket數組
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
//正在擴容
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
//遷移元素
//新桶數組大小是舊桶數組的兩倍
//遷移元素會鎖住當前bucket,也是分段鎖
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
4、總結
1、ConcurrentHashMap採用的鎖有分段鎖,synchronized,CAS,自旋鎖,volatile等;
2、JDK1.7中使用的是ReentrantLock,此處使用的是synchronized,說明JDK1.8中對synchronized進行了極大的優化;
3、ConcurrentHashMap中沒有threshold和loadFactor這兩個字段,而是用sizeCtl字段來控制擴容;
4、插入操作採用分段鎖的思想,使用synchronized鎖住當前bucket的第一個Node;
5、擴容過程是採用CAS控制sizeCtl這個字段來進行的。
結束語
限於篇幅,本篇只介紹了ConcurrentHashMap的put方法,此方法包含了ConcurrentHashMap的精華,主要是瞭解其本質,學習它的設計思路,這也是我們讀源碼的目的之一。