JDK 1.7
- JDK 1.7 中的 ConcurrentHashMap 最外层是多个 Segment,每个 Segment 的底层数据结构与 HashMap 类似,仍然是数组和链表组成的拉链法。
- 每个 Segment 独立上 ReentrantLock 锁,每个 Segment 之间互不影响,提高了并发效率。
- ConcurrentHashMap 默认有 16 个 Segments,所以最多可以同时支持 16 个线程并发写(操作分别分布在不同的 Segment 上)。这个默认值可以再初始化的时候设置为其他值,但是一旦初始化以后是不可以扩容的。
- ConcurrentHashMap 不允许 key 为 null。
JDK 1.8
- ConcurrentHashMap 1.8 结构和 HashMap 类似
- 初始化的时候通过自旋、 CAS 和双重 check 方法保证线程安全
- put 的时候通过以下几点保证线程安全
- 自旋死循环保证必须新增成功;
- 指定位置上的槽点为空的时候,使用 CAS 生成第一个 Node,防止重复生成;
- 槽点有值的时候,使用 synchronized 进行加锁,锁住当前位置的第一个 Node,如果是 TreeBin 锁住的就是整棵树;
- 在增加节点并且需要重新维护红黑树的节点的时候,或者删除树节点的时候,需要 lock 住 TreeBin 的 lockState,网上都说是保证树维护平衡的操作出现并发情况,准确来说主要是用于 get 的时候判断使用 TreeBin 的查询方法还是使用链表的查询方法,因为基本上所有的维护平衡的操作都在 synchronized 里,所以写入不会冲突,但是树旋转的时候可能会改变根节点或者链接,而读操作永远不应该被阻塞,所以在维护红黑树平衡的时候锁住 lockState。
- 树化 treeifyBin 方法,是使用 synchronized 与双重 check 保证线程安全,在没构建好树的时候,get 取得还是链表里的数据,会在构建好之后把链表替换掉
- get 的几种情况:
- 是 TreeBin:
- 当 lockState 是等待或者写(WAITER | WRITER)的时候,使用红黑树维护的链表进行查询,速度慢,但是读写不互斥,所以我认为 lockRoot 方法改变 lockState 就是用在这;
- 否则就是使用树的查找操作,速度快,会加读锁,可能会唤醒因为读锁而阻塞的线程;
- 是 ForwardingNode,到 nextTable 上查找;
- 是链表,递归往后遍历,找需要的值。
- 是 TreeBin:
- size 方法的实现和 LongAdder 类似,有 cellsBusy 控制新增数据时,count 加在 baseCount 或者 counterCells 数组里,最后取 baseCount 和 counterCells 里的值加一块。
- 扩容 transfer 的时候, 可以是多线程协助进行扩容,并且不会出现线程安全问题
- 在 new Node 赋值给 nextTable 不会出现并发问题的原因:在所有能进入 transfer 方法之前会用 CAS 的方式把 sizeCtl 设置为 (rs << RESIZE_STAMP_SHIFT) + 2) ,而 rs 是 resizeStamp(n) 方法得到的,这个方法上写着“左移”RESIZE_STAMP_SHIFT 肯定为负,而为负的话,就只会进入协助扩容的代码里去。
- 多线程扩容的时候:因为每个线程处理扩容是一批一批进行的,需要处理哪个位置到哪个位置的数据是从后往前处理,先分配再处理,分配到哪个位置用 transferIndex 用 CAS 的方式更新
- 不会和新增删除出现并发问题:处理的时候用 synchronized 加锁指定下标的 Node
- 不会阻塞读取操作:
- 在新建了 ForwardingNode ,并且没获取到锁的时候
- 可能新增删除拿到锁了,安全的原因看 put 方法
- 可能没人拿到锁,还没执行到加锁,那 get 的时候按正常的 get 方法,用 ForwardingNode 转移到新数组上去查询
- 拿到锁的时候
- 指定位置还没设置成 ForwardingNode ,那么正常的 get
- 设置成 ForwardingNode ,那么查询按照 ForwardingNode 的查询方法
- 在新建了 ForwardingNode ,并且没获取到锁的时候
- 先写后读不阻塞、先读可能会阻塞另外线程的写,读读不阻塞,写写阻塞。
put 方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key value 不允许为 null
if (key == null || value == null) throw new NullPointerException();
// 计算 hash 值
// -1为ForwardingNode表示正在扩容,-2为TreeBin表示桶内为红黑树,大于0表示桶内为链表。)
int hash = spread(key.hashCode());
// 用来记录所在table数组中的桶的中链表的个数,后面会用于判断是否链表过长需要转红黑树
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 懒加载,没有初始化才初始化,进行下一次循环
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 已经初始化过,但是这个位置还没有任何 Node 数据
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// CAS 设置 Node,成功就结束循环,否则下次循环
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果头节点 hash 值为-1,则为 ForwardingNode 节点,说明正在扩容
else if ((fh = f.hash) == MOVED)
// 调用hlepTransfer帮助扩容
tab = helpTransfer(tab, f);
// 目前槽点有值的往下执行
else {
V oldVal = null;
// 锁定当前槽点 / 红黑树(TreeBin),每个槽点是独立的
synchronized (f) {
// 再检查一次加锁之前,这个索引位置的槽点的头节点是否被修改了
if (tabAt(tab, i) == f) {
// 大于0 是链表
if (fh >= 0) {
// binCount 链表里是节点的个数
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 这个下标的 Node 链表里有同样的 key 的值,直接替换
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
// 新的 key,那么创建新的 Node 放在链表尾部,退出循环
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;
}
}
}
// 添加 size ,可能会进行扩容
addCount(1L, binCount);
return null;
}
putTreeVal 方法
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
// 根节点为 null,创建根节点,退出循环
if (p == null) {
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
// 当前节点的 hash 值大于要新增的节点,等会往左走
else if ((ph = p.hash) > h)
dir = -1;
// hash小于往右走
else if (ph < h)
dir = 1;
// key 相同直接返回了
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
// 当前节点的 key 和要插入的 key hash 值相同,但是 equals 不成立
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
// 递归寻找,得到了与更新的 key 相同的节点直接返回
if (((ch = p.left) != null &&
(q = ch.findTreeNode(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
// 没找到,比较要插入的 key 和 当前节点的 key 返回 -1 或 1,表示左右子节点
dir = tieBreakOrder(k, pk);
}
// xp = 当前节点p
TreeNode<K,V> xp = p;
// 直到 当前节点p = 右边的或者左边的子节点,否则进行下次循环
if ((p = (dir <= 0) ? p.left : p.right) == null) {
TreeNode<K,V> x, f = first;
// 红黑树还维护了双向链表,每次新生成的 TreeNode 都会变成新的 头节点 first,因为查询的时候可能会使用链表形式查询
// 生成新的要添加的TreeNode节点,并且赋值给头节点first和x
first = x = new TreeNode<K,V>(h, k, v, f, xp);
// 如果原来的头节点不为空,把头节点挂到新增的节点后面
if (f != null)
f.prev = x;
// dir < = 0 ,把新增节点挂到它父节点(当前节点)的左子节点
if (dir <= 0)
xp.left = x;
// 否则把新增节点挂到它父节点(当前节点)的右子节点
else
xp.right = x;
// 如果新增节点的父节点是黑色,则新增节点为红色
if (!xp.red)
x.red = true;
// 如果新增节点的父节点是红色,则需要加锁重新维护红黑树的红黑平衡
else {
// 使用 CAS 锁住 lockState 还可能用上LockSupport
lockRoot();
try {
root = balanceInsertion(root, x);
} finally {
unlockRoot();
}
}
break;
}
}
assert checkInvariants(root);
return null;
}
get 方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 与头节点 key 相同,返回结果
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 小于0,是红黑树(TreeBin)或者正在扩容(ForwardingNode)
else if (eh < 0)
// 调用TreeBin / ForwardingNode 的 find方法
return (p = e.find(h, key)) != null ? p.val : null;
// 这里表示是链表,递归找链表上的数据
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
TreeBin.find 方法
final Node<K,V> find(int h, Object k) {
if (k != null) {
for (Node<K,V> e = first; e != null; ) {
int s; K ek;
// 如果锁状态为 写或者等待,使用红黑树维护的链表进行查询
// 效率慢,但是读写不互斥
if (((s = lockState) & (WAITER|WRITER)) != 0) {
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next;
}
// 否则,使用红黑树的查询方法,查询速度快
// CAS lockState + 4
else if (U.compareAndSwapInt(this, LOCKSTATE, s,
s + READER)) {
TreeNode<K,V> r, p;
try {
// 红黑树查询方式
p = ((r = root) == null ? null :
r.findTreeNode(h, k, null));
} finally {
Thread w;
// 减去自己线程的读锁
if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
// 如果存在因为读锁而阻塞的线程,唤醒线程
(READER|WAITER) && (w = waiter) != null)
LockSupport.unpark(w);
}
return p;
}
}
}
return null;
}
transfer 方法
ForwardingNode:主要是在扩容的时候,新数组的数据还没有完全设置完毕,那么就不可以把 ConcurrentHashMap 的 table 设置成新数组,这个时候查询的时候是通过 ForwardingNode 转移到新数组上去查询。
扩容原理更详细的博文:blog.csdn.net/ZOKEKAI/art…
入口
- 每次添加完后,调用的 addCount 中有调用 transfer 扩容
- 桶中链表大于 8 调用 treeifyBin 方法转红黑树的方法的时候,在该方法中会判断 table 当前总容量是否大于64,如果 table 当前总容量小于 64,不会转红黑树,而是调用 tryPresize 方法尝试扩容,tryPresize 方法中会调用 transfer 扩容
- 在 putVal 发现 头节点的 hash 为 MOVED(-1)也就是这个节点为 ForwardingNode 的时候,会调用 helpTransfer 然后 transfer 方法扩容
addCount 方法
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
...
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 当前容量大于扩容阈值,并且长度小于最大限制,进行扩容
// sizeCtl 在进行扩容完成之后的作用就是存放扩容阈值,而如果在扩容的时候,sizeCtl是一个负数,也会满足条件
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
/**
* Returns the stamp bits for resizing a table of size n.
* Must be negative when shifted left by RESIZE_STAMP_SHIFT.
* 左移 RESIZE_STAMP_SHIFT 肯定为负
*/
int rs = resizeStamp(n);
// 小于0,表示有其他线程正在扩容
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// CAS 的方式设置 sizeCtl 为一个负数
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 扩容
transfer(tab, null);
s = sumCount();
}
}
}
transfer 方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 计算每条线程处理的桶个数,每条线程处理的桶数量一样,如果是单核CPU,则使用一条线程处理所有通
// 每条线程最少处理 16 个桶,如果计算出来的结果少于 16,则一条线程处理 16 个桶
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 单线程扩容 或者 第一条线程,nextTab 肯定为 null。协助线程是不会满足条件的
if (nextTab == null) { // initiating
try {
// 创建一个长度是当前数组长度两倍的新的数组
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
// 扩容保护
sizeCtl = Integer.MAX_VALUE;
return;
}
// 把新建的数组赋值给 nextTable
nextTable = nextTab;
// 扩容总进度,指向原数组索引最大的位置
transferIndex = n;
}
int nextn = nextTab.length;
// 这个节点的 hash 固定是 MOVED(-1)
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
// bound 是这次任务的最后一个位置,即自己要处理的最小的下标,i 是在分配任务之后最大的下标数,即开始的位置
// 是从后往前处理
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 1. 分配任务,这个线程负责转移哪几个位置的桶
while (advance) {
int nextIndex, nextBound;
// 往前推进一个位置,然后进行判断是否到了自己要处理的最前面那个位置
// 如果到了或者 finishing 标识为为 true,表示处理完了,没有到继续处理 i 位置
if (--i >= bound || finishing)
advance = false;
// transferIndex <= 0 说明被分配完了,结束 while 循环
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 只有首次进入 for 循环才会进入这个判断,设置了 i 和 bound 的值
// 比如 transferIndex 本来是 100 ,stride 是 16 ,那么该线程领取到的任务就是 84 ~ 99 位置的槽点的转移任务
// transferIndex 就被用 CAS 的方式设置成 84
// 下次来了协助线程,继续分配 68 ~ 83 位置的槽点
// 范围为 [bound, nextIndex)
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
// 下标从 0 开始,所以 - 1
i = nextIndex - 1;
advance = false;
}
}
// n 是原数组的长度,nextn 是新数组的长度
// i < 0 表示这个线程拿到的范围是 [0, X) 并且执行完了或者没拿到(包括自己的执行完了,然后再次 while 循环没拿到任务)
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 所有线程干完活了
if (finishing) {
nextTable = null;
// 替换新的 table
table = nextTab;
// 调 sizeCtl 为旧容量的 1.5 倍
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 到这就是表示这个线程的任务完成了 sizeCtl - 1,表示参与扩容的线程 -1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
/**
第一个扩容的线程,执行transfer方法之前,会设置 sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2)
后续帮其扩容的线程,执行transfer方法之前,会设置 sizeCtl = sizeCtl+1
每一个退出transfer的方法的线程,退出之前,会设置 sizeCtl = sizeCtl-1
那么最后一个线程退出时:
必然有sc == (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2),即 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
*/
//不相等,说明不到最后一个线程,直接退出transfer方法
// 到最后一个线程就设置 finishing 为 true 下次循环的时候 用新 hash 表覆盖旧 hash 表
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
// 这个位置没有任何 Node 设置一个占位对象
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// hash 为 MOVED 说明已经被处理,继续回去执行第 1 步,分配任务
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 链表结构
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
// 从头节点到最后一个节点,使用头插法拼接链表
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 将 ln 设置到新数组下标为 i 的位置上
setTabAt(nextTab, i, ln);
// 将 hn 设置到新数组下标为 i +n 的位置上
setTabAt(nextTab, i + n, hn);
// 把刚才创建的ForwardingNode设置到原来的的hash桶上
// 作用是标识该位置已经被处理过,以及后来查询的转发作用
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
// 如果 p 点没有前置节点
if ((p.prev = loTail) == null)
lo = p;
// 如果 p 有前置节点那么把 loTail 的下一个节点设置为 p
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 迁移之后这个树上的数据可能会变少,符合条件转为红黑树,不符合就使用链表
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}