前言
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的精华,主要是了解其本质,学习它的设计思路,这也是我们读源码的目的之一。