ConcurrentHashMap(1.8版本)
问题
问题和解答可以作为对ConcurrentHashMap的总体了解。
1. ConcurrentHashMap高效在哪里?
java8版本的ConcurrentHashMap的高效体现在两部分:
- 数据结构上:使用了哈希表+链表+红黑树,即使在大量的哈希碰撞下也可以快速的检索键值对。
- 并发上:
- 在数组扩容时:支持多线程并发扩容,增强扩容的效率;同时扩容时可以进行读操作,完全不会堵塞,提高读效率;写操作会分成两部分,若写入的对应数组槽位还没有被转化,则可以正常写入,否则该线程会暂停写入,并参与到扩容转化的过程,直到扩容结束后继续进行写操作。进一步细化了写的锁粒度,提高写效率。
- 并发写时:只对哈希冲突的键值对造成影响,上锁也只上对应槽位的锁,对数组其他槽位没有影响,降低了锁的粒度。
- 在初始化数组和初始化槽位,以及更新锁状态时,大量运用了CAS乐观锁,避免了阻塞、唤醒的开销。
数据结构内部类介绍
- Node
- TreeNode
- TreeBin
- ForwardingNode
Node
/**
* Node是用于存放键值的最基本单位
* Node是单向链式结构
*/
static class Node<K,V> implements Map.Entry<K,V> {
/**
* 一般是键的哈希值
* 也会被用于其他标识,用于其他特殊标识时小于0
* 若为-1表示当前节点所在的槽位正在初始化或扩容中。
* 若为-2表示该节点是树状架构。
*/
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(); }
/**
* 查找指定键值。
* 默认是链式查找。
* 该类子类都会实现对应的find方法来实现各种数据结构的查找节点功能。
*/
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;
}
/*下面是一些通用的方法,减少子类重复的代码*/
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)));
}
}
TreeNode
/**
* TreeNode是红黑树结构的树状节点,用于存储键值对
* 该节点继承了Node,保留了键值,哈希的属性。
*/
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
/**
* 重写了find方法,用于实现树状结构的查找结点方法
*/
Node<K,V> find(int h, Object k) {
return findTreeNode(h, k, null);
}
/**
* 核心查找值得实现方法
*/
final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
if (k != null) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk; TreeNode<K,V> q;
TreeNode<K,V> pl = p.left, pr = p.right;
// 比较当前节点哈希和检索的键哈希
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
// 哈希相同,比较键值
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p; // 找到了对应的键,返回值
// 如果哈希相同,键值不同,则通过null值进行排除
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
// 无法排除,则尝试通过强制比较进行比对
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
// 尝试通过右子树查找
else if ((q = pr.findTreeNode(h, k, kc)) != null)
return q;
else // 排除了所有可能性,只能从左子树查找
p = pl;
} while (p != null);
}
return null;
}
}
TreeBin
/**
* TreeBin是树状结构的管理节点,是一个标志和管理节点,该节点不存储键值对。
* 通常遍历槽位第一节点时通过判断该节点是否是TreeBin类来判断该槽位是否是树状结构。
* TreeBin的hash属性存的值是-2。
* TreeBin还负责对树的独写上锁控制,红黑树的维护。
*/
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root; // 树状结构真正的根节点
/**
* TreeNode继承于Node,因此也包含了链式结构。
* 树状结构初始化时,先传入链式结构的TreeNode交给TreeBin,
* TreeBin再负责转化为红黑树。
* 这里first是链式结构的头节点。
*/
volatile TreeNode<K,V> first;
/**
* 被阻塞的线程
*/
volatile Thread waiter;
/**
* 锁状态
* 配合下面标志位使用
*/
volatile int lockState;
/* 这里是二进制标志位的锁控制 */
static final int WRITER = 1; // 写锁:0001
static final int WAITER = 2; // 有线程等待:0010
// 读锁:0100
// 读锁是共享锁,因此多个读锁可以兼容。
// 这里多个读锁lockState只用每次加4
// 判断是否有人持有读锁只需要把lockstate除以4,看看是否大于0即可。
static final int READER = 4;
/**
* 这个方法在对象a和对象b无法直接比较的情况下
* 提供一个相对统一的比较标准。
*/
static int tieBreakOrder(Object a, Object b) {
int d;
// 如果a或b是空,
// 则比较a和b的类名
// 若a和b类名相同,则使用默认的系统哈希计算方法。
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
/**
* 传入链式结构的TreeNode
* 生成树状结构的TreeNode,并由TreeBin管理
*/
TreeBin(TreeNode<K,V> b) {
// static final int TREEBIN = -2;
super(TREEBIN, null, null, null);
this.first = b;
TreeNode<K,V> r = null;
for (TreeNode<K,V> x = b, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
// 根节点
if (r == null) {
x.parent = null;
x.red = false;
r = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = r;;) {
int dir, ph;
K pk = p.key;
// 通过哈希比较
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
// 通过comparable比较
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
// 强制比较
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
// 判断是否需要调整红黑树结构
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
r = balanceInsertion(r, x);
break;
}
}
}
}
this.root = r;
assert checkInvariants(root);
}
/**
* 对树上锁
*/
private final void lockRoot() {
// 用CAS上锁
if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
contendedLock(); // 竞争上锁
}
/**
* 解锁
*/
private final void unlockRoot() {
lockState = 0;
}
/**
* 竞争上锁
*/
private final void contendedLock() {
boolean waiting = false;
for (int s;;) {
// ((s = lockState) & ~WAITER) == 0 表示读锁和写锁都没锁
if (((s = lockState) & ~WAITER) == 0) {
// 尝试上锁
if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
if (waiting)
waiter = null;
return;
}
}
// 如果锁已被其他线程获取
// 判断是否有线程在等待
else if ((s & WAITER) == 0) {
// 来到这表示获取锁被其他线程获取,
// 但没有人等待获取锁,则等待线程设为当前线程
if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {
// 设为等待状态
waiting = true;
waiter = Thread.currentThread();
}
}
// 如果已处于等待的状态,则阻塞
else if (waiting)
LockSupport.park(this);
// 若所有if都没踩中,表示当前这棵树有竞争,且不止一个,
// 本线程会一直自旋直至能够设为等待线程或者获取到锁。
}
}
/**
* 重写了Node的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;
}
// 若没被上写锁,则尝试加读锁,并进行树状搜索。
else if (U.compareAndSwapInt(this, LOCKSTATE, s,
s + READER)) {
TreeNode<K,V> r, p;
try {
// 树状搜索调用树状节点的find方法。
// 树状搜索都是从树的头节点开始搜索。
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;
}
/**
* 移除指定节点
* @return 如果树很小则返回true,反则返回false
*/
final boolean removeTreeNode(TreeNode<K,V> p) {
TreeNode<K,V> next = (TreeNode<K,V>)p.next;
TreeNode<K,V> pred = p.prev;
TreeNode<K,V> r, rl;
// 表示要去除的节点如果是头节点
if (pred == null)
first = next; // 修改头节点
else
pred.next = next; // 从节点链表中剔除
if (next != null)
next.prev = pred;
// 若first为空,表示整棵树为空树
if (first == null) {
root = null;
return true;
}
if ((r = root) == null || r.right == null ||
(rl = r.left) == null || rl.left == null)
// 来到这里表示这棵树太小
return true;
lockRoot(); //对整棵树上锁
try {
TreeNode<K,V> replacement;
TreeNode<K,V> pl = p.left;
TreeNode<K,V> pr = p.right;
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
// 寻找被替换节点的右子树的最左非空节点
while ((sl = s.left) != null)
s = sl;
// 下面是在红黑树中删除节点
boolean c = s.red; s.red = p.red; p.red = c;
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // 如果s是p的直接右节点
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
r = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
r = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
root = (p.red) ? r : balanceDeletion(r, replacement);
if (p == replacement) { // detach pointers
TreeNode<K,V> pp;
if ((pp = p.parent) != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
p.parent = null;
}
}
} finally {
unlockRoot();
}
assert checkInvariants(root);
return false;
}
/* ------------------------------------------------------------ */
// Red-black tree methods, all adapted from CLR
/**
* 这个方法用于维护红黑树的性质
*/
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
// 此处忽略维护红黑树的代码逻辑
}
/**
* 这个方法用于维护红黑树的性质
*/
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
// 此处忽略维护红黑树的代码逻辑
}
/**
* 这个方法用于增加节点后维护红黑树的性质
*/
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
// 此处忽略维护红黑树的代码逻辑
}
/**
* 这个方法用于删除节点后维护红黑树的性质
*/
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
TreeNode<K,V> x) {
// 此处忽略维护红黑树的代码逻辑
}
/**
* 检查链式结构和树状结构的准确性
*/
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode<K,V>)t.next;
if (tb != null && tb.next != t)
return false;
if (tn != null && tn.prev != t)
return false;
if (tp != null && t != tp.left && t != tp.right)
return false;
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
if (t.red && tl != null && tl.red && tr != null && tr.red)
return false;
if (tl != null && !checkInvariants(tl))
return false;
if (tr != null && !checkInvariants(tr))
return false;
return true;
}
private static final sun.misc.Unsafe U;
private static final long LOCKSTATE;
static {
try {
U = sun.misc.Unsafe.getUnsafe();
Class<?> k = TreeBin.class;
LOCKSTATE = U.objectFieldOffset
(k.getDeclaredField("lockState"));
} catch (Exception e) {
throw new Error(e);
}
}
}
ForwardingNode
/**
* 这是一个特殊的控制节点
* 当数组正在扩容时,旧数组已移植的槽位会被换上该类节点
* 该类节点保存了新的数组,旧数组可以通过该节点访问到新数组的数据。
* 一、 在读取值时存在两种情况:
* 1. 若数组转化过程中还未转化到或正在转化指定槽位:
* 访问旧数组该槽位时,要么是链式节点要么是树状节点,可以照常访问;
* 2. 若数组转化已转化完指定槽位:
* 访问旧数组指定槽位时会发现该槽位被换成ForwardingNode节点,可以通过该节点去到新数组并访问新数组的值。
*
* 二、 在写值时存在两种情况:
* 1. 第一种情况和读类似,因为没有感知到节点正在扩容,因此可以正常插入新值。
* 2. 第二种情况,发现正在扩容时,则调用helpTransfer方法,参与共同扩容。扩容后新数组已变成默认数组,可以正常读取。
*/
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
/**
* 重写了Node的find方法
* 可以间接从旧数组访问到新数组一转化好的槽位。
*/
Node<K,V> find(int h, Object k) {
// 在新的数组查找
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
// 节点非链式结构
if (eh < 0) {
// 节点是ForwardingNode类型,说明又一次发生了扩容
if (e instanceof ForwardingNode) {
// 获得最新的ForwardingNode节点。
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
return e.find(h, k); // 调用对应的查找方式
}
// 链式查找
if ((e = e.next) == null)
return null;
}
}
}
}
存取机制
put方法
概述
- put方法用于存放键值,该方法并发安全。
代码
public V put(K key, V value) {
// 核心代码是调用了putVal方法。
return putVal(key, value, false);
}
putVal
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 不允许存储null值
// ConcurrentHashMap的null值默认表示取不到值。
if (key == null || value == null) throw new NullPointerException();
// 在key的hash基础上进一步对哈希进行处理,
// 使哈希更加均匀的散步到table的槽位上。
int hash = spread(key.hashCode());
int binCount = 0; // 用于控制addCount方法的操作位
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果table还是空值,则先初始化。
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化代码放在后面。
// (n - 1) & hash 可以把hash映射到索引上
// 这里把获取槽位和判断语句合在一起,可以精简代码。
// 这个分支判断该槽位是否已经放有节点。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果该槽位为空,则通过cas插入该键值节点。
// 节点构造函数的参数依次是:哈希、键、值、下一个节点
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
// 哈希是MOVED是一个特殊的节点ForwardingNode
// 该节点表示当前这个槽位的节点正在扩容转化中。
// 这个分支是判断该槽位是否处于转化状态。
else if ((fh = f.hash) == MOVED)
// 该线程参与到转化过程中。
// ConcurrentHashMap支持并发转化。
tab = helpTransfer(tab, f);
// 排除上面可能,则开始放值。
else {
V oldVal = null; // 如果该键已存在,这个变量用于放原来的值。
synchronized (f) { // 锁当前槽位
if (tabAt(tab, i) == f) { // 双重检验
// 哈希大于零则是链表结构
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;
// 通过内置的TreeBin类方法插入新值。
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);
// oldVal为空表示该键本身已存在,
// 这里只替换了旧值,并没有插入新值。
if (oldVal != null)
return oldVal;
break;
}
}
}
// 重新统计数量。
addCount(1L, binCount);
return null;
}
initTable方法
// 用于初始化table
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// sizeCtl小于0表示table正在初始化
if ((sc = sizeCtl) < 0)
Thread.yield(); // 本线程暂时退让,等待table初始化完毕
// 通过CAS把sizeCtl替换成-1
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// sizeCtl大于0时用于记录table的大小。
// 若sizeCtl没有被初始化,则使用默认值16.
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
// 最后sizeCtl保存需要扩容数组的元素数量临界值。
sizeCtl = sc;
}
break;
}
}
return tab;
}
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) {
// 比较头节点哈希
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 如果头节点是树状结构或者是转化中状态
else if (eh < 0)
// 调用各自类型节点实现的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;
}
数组扩容机制
控制属性
/**
* 数组初始化和重组控制符。
* 负数时:数组在初始化时为-1,若是在重组,则该值的相反数减1是参与转化的线程数。
* 正数时:该值表示数组需要被重组时的元素数量。
* 等于0:数组为空或还没被初始化。
*/
private transient volatile int sizeCtl;
private static final long SIZECTL; // sizeCtl的指针。
/**
* 最小步幅
* 表示并发转化下,每次分配给线程的最少的槽位数。
*/
private static final int MIN_TRANSFER_STRIDE = 16; // 最小步幅
/**
* 用于生成重组标记。
*/
private static int RESIZE_STAMP_BITS = 16;
/**
* 最多帮忙转化的线程数。
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
/**
* 在sizeCtl中记录大小标记的位移位。
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
Transfer方法
概述
- transfer方法是对当前数组进行扩容,通常扩容到当前大小的两倍,且扩容前后大小都是2的幂次方。
- tranfser方法支持多线程,因此有个配套的helpTransfer方法。sizeCtl属性的也会记录参与transfer的线程数量。
- 特殊的头节点ForwardingNode:该节点头的hash是-1,表示当前槽位正在转化过程中。该特殊节点包含了新的数组,所有的旧数组槽位若被转化过,都会被替换成这个特殊节点,从而不管访问哪个就节点都能快速找到到新的数组,并以特殊的针对转化中方法去寻找对应的键值对。
- transfer方法有个概念叫“步幅”,步幅定义了一个线程一次可以分配多少个数组槽位给当前线程转化。步幅最小是16。几个属性和变量:
- stride:步幅,通过当前电脑CPU所拥有的核心数,以及当前数组大小进行计算,得出一个合理的步幅值。
- TRANSFERINDEX、transferIndex:transferIndex是全局变量,用来表示当前数组槽位以分派到哪一个槽位。分派方式是从高索引往低索引分派。TRANSFERINDEX是指向transferIndex的指针,是为了更好的提高该属性的线程可见度,及时感知该值是否被修改。
- bound:界限,bound定义了当前线程所转化的槽位的界限,若转化的索引到达了bound,则需要重新申请新的一组转化槽位。
过程
- transfer方法会申请是当前数组两倍大小的新数组。
- 生成特殊节点ForwardingNode。
- 申请当前线程转化的数组槽位。
- 遍历需要转化的槽位,遇到几种情况:
- 该槽位是null,给他赋上ForwardingNode。
- 该槽位已经是ForwardingNode,那么表示该槽位已经被转化过了或者已经有线程正在转化它,当前线程直接跳过。
- 不是上面两种情况,则当前线程对该槽位的头节点上锁,并遍历当前槽位的节点并转化到新数组中,把当前槽位的节点替换成ForwardingNode。
- 若当前线程转化完当前的所有数组槽位,则重新申请新的数组槽位进行转化。
- 转化完成后,把新数组设为默认数组,设置新的阈值:新数组大小的0.75倍。
- 转化结束。
代码
// 转化时用于分割不同线程下的数组槽位。
// 当线程申请转化的数组槽位时,transferIndex就会向左移动步幅的大小,切割出一部分槽位交给该线程转化
private transient volatile int transferIndex;
private static final long TRANSFERINDEX; // 它是transferIndex的指针,可以看成就是transferIndex本身。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 下面if用于计算步幅
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
// 初始化新的数组
if (nextTab == null) {
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 = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
// 特殊的节点,保存有新的数组,用于标记数组槽位正在转化操作。
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; // 用于控制线程访问数组槽位的标记
boolean finishing = false; // 用于标记转化是否完成
// i是用于遍历数组的槽位,一般从高索引向低索引遍历。
// i不是连续顺序遍历,是在步幅范围内顺序,但步幅与步幅间可能存在跳跃。
// bond定义了当前的线程转化的槽位的界限。
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
// i在bound界限之内,顺序执行。
if (--i >= bound || finishing)
advance = false;
// i不在界限之内,则判断transferIndex是否已经分割完槽位,若分割完则停止获取转化槽位。
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 若还有未分割的槽位
// 则通过CAS尝试获取用于本线程转化的槽位。
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 先判断完成转化的标志位
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab; // 新的数组设为默认数组
sizeCtl = (n << 1) - (n >>> 1); // 设置新的阈值,0.75倍新数组大小
return;
}
// 设置变量参数sizeCtl
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 若满足下面的条件,则表示当前线程是最后一个转化线程,其他转化线程已全部结束。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n;
}
}
// 当前槽位还没有初始化,用CAS初始化成ForwardingNode节点。
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 哈希值是MOVED则是ForwardingNode节点。
// 若当前节点已经是ForwardingNode节点,则表示该槽位已被转化,跳过。
else if ((fh = f.hash) == MOVED)
advance = true;
// 排除了其他可能性,开始转化当前槽位。
else {
synchronized (f) { // 上锁
// 双重校验
// 当前线程获取到锁时,其他线程已经把这个槽位给转化了。
// 若被转化过,该节点会变成ForwardingNode。
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 判断是不是链表节点
if (fh >= 0) {
// 由于ConcurrentHashMap的特殊映射机制,
// 若分配到原索引槽位,runbit则为0,
// 若分配到新索引槽位(该槽位只可能是旧索引+就数组大小),则为1。
int runBit = fh & n;
Node<K,V> lastRun = f;
// 这个for循环会取出最后一段节点,这段节点都是放置在同一个槽位的,
// 可能是新索引槽位,也可能是旧索引槽位,这一段节点可能也只有一个节点。
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);
}
// 更新两个对应的索引槽位
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
// 就数组槽位设为ForwardingNode节点。
// 当转换为ForwardingNode节点表示该槽位已经转换完毕。
setTabAt(tab, i, fwd);
// 该槽位处理完毕,重新获取新的待转化槽位。
advance = true;
}
// 若该槽位的节点是树状结构
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f; // 旧数组槽位中的树
// ConcurrentHashMap的树状结构节点也保留了列表的结构。
// 一开始初始化先是转化成列表结构
// 在根据节点数量判断是否要转化成树状结构。
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) {
// 若没有初始化,则初始化
if ((p.prev = loTail) == null)
lo = 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;
}
}
}
}
}
}
helpTransfer方法
/**
* 该方法可以使当前线程参与到transfer中。
* @param tab 数组
* @param f 槽位头节点
*/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 数组不为空,且槽位中的头节点是ForwardingNode节点
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;
// 若没有结束,sc+1标志多一个线程参与转化
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
tryPresize方法
/**
* 尝试扩容数组
*/
private final void tryPresize(int size) {
// 限制最大容量
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
// 数组还没有被初始化,先初始化
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c; // sc和c取大者作为容量
// 把sizeCtl设为-1状态,标志数组正在重组
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
// c还小于重组阈值或者当前大小已经达到最大的数组大小,
// 则不扩容数组。
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
int rs = resizeStamp(n); // 获取重组标记
// 若数组正在扩容
if (sc < 0) {
Node<K,V>[] nt;
// 判断是否还在重组
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);
}
// 扩容数组
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
树化和逆树化
相关属性
/**
* 树化阈值
* 当一个槽位的节点数大于该阈值时,
* 则对该槽位的链式节点转化为树状结构
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 逆树化阈值
* 当一个槽位的节点数小于于该阈值时,若该结构是树状结构,
* 则转化为链式结构。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 最小树化阈值
* 当槽位节点达到树化标准而数组大小没有达到该阈值时
* 则通过扩容数组来避免树化操作。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
treeifyBin方法
/**
* 用于把链式节点转化成树状节点
* tab 对应的数组
* index 要转化的索引槽位
*/
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// 如果没有达到树化最小阈值
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1); // 扩容数组代替树化操作。
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) { // 上锁
if (tabAt(tab, index) == b) { // 双重校验
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
// 生成对应的树状节点
// 树状节点依托于链式节点,既有前后访问的链式结构,
// 也有左右子孩子的树状结构。
// 这里的循环先初始化链式结构
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
// treeBin是树状结构的头节点
// 把树状节点的树状结构搭建起来的操作在TreeBin的构造器里进行。
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
/**
* 这是TreeBin的构造器
* 包含了把链式节点转化为红黑树的过程
*/
TreeBin(TreeNode<K,V> b) {
super(TREEBIN, null, null, null);
this.first = b;
TreeNode<K,V> r = null;
// 遍历链式结构的树状节点
for (TreeNode<K,V> x = b, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (r == null) { // 初始化根节点
x.parent = null;
x.red = false;
r = x;
}
else { // 构建子树
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = r;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
r = balanceInsertion(r, x); // 平衡红黑树结构
break;
}
}
}
}
this.root = r; // TreeBin类会持有根节点
assert checkInvariants(root); // 检查树状结构是否正确。
}
/**
* 检查t的树状结构是否正确。
*/
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode<K,V>)t.next;
if (tb != null && tb.next != t)
return false;
if (tn != null && tn.prev != t)
return false;
if (tp != null && t != tp.left && t != tp.right)
return false;
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
if (t.red && tl != null && tl.red && tr != null && tr.red)
return false;
if (tl != null && !checkInvariants(tl))
return false;
if (tr != null && !checkInvariants(tr))
return false;
return true;
}
untreeify
/**
* 逆树化操作
*/
static <K,V> Node<K,V> untreeify(Node<K,V> b) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = b; q != null; q = q.next) {
Node<K,V> p = new Node<K,V>(q.hash, q.key, q.val, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
计数机制
- ConcurrentHashMap的计数机制由一个基础值加上一个数组里面的所有值得到总和。
相关属性和类
/**
* 基础计数值
* 若没有发生竞争情况,则使用这个值计数。
*/
private transient volatile long baseCount;
/**
* baseCount的指针
*/
private static final long BASECOUNT;
/**
* 用于并发情况下的计数单位
*/
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
/**
* 用于并发计数数组
* 增加时,线程随机映射到该数组的一个槽位,并把里面的值加上增加的数值。
*/
private transient volatile CounterCell[] counterCells;
/**
* 自旋锁,用于锁CounterCell数组
*/
private transient volatile CounterCell[] counterCells;
private static final long CELLSBUSY; // counterCells的指针
addCount方法
/**
* 添加ConcurrentHashMap记录的元素数量
* x 增加的数量
* check 控制数
* 1.小于0时不检查是否需要扩容;
* 2.小于等于1时,若不发生竞争则检查;
* 3.非以上情况则检查。
*/
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 如果CounerCell还没有初始化且修改baseCount没有遇到竞争,
// 则直接把新增数量加到baseCount中。
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// 来到此处表示CounerCell初始化过或者存在多个线程竞争添加数量
CounterCell a; long v; int m;
boolean uncontended = true;
// 如果CounerCell还没初始化,
// 或者随机获取一个CounerCell槽位,该槽位还没初始化,
// 或者该槽位已经初始化了,但修改该槽位的值时发生了竞争
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
// 运行到这表示本次增加次数没有遇到竞争
if (check <= 1)
return;
s = sumCount(); // 计算总和
}
// 扩容数组
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
// 若sc小于0表示当前数组已经在重组中
if (sc < 0) {
// 查看重组标志是否一致,若不一致表示没有在重组或本轮重组已结束
// transferIndex <= 0 表示重组是否已经结束
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 协助重组的线程数加1,
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 这里表示数组没有被重组,当前线程开始对数组进行重组
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount(); // 求出ConcurrentHashMap的元素数量
}
}
}
sumCount方法
/**
* 计算ConcurrentHashMap存储的元素数量
* 通过遍历CounterCell数组,把值累加,最后加上baseCount即得到结果。
*/
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;
}
fullAddCount方法
/**
* 添加值
* 该方法用于竞争十分激烈的时候
* 或者用于初始化CounterCell数组或指定槽位。
*/
private final void fullAddCount(long x, boolean wasUncontended) {
// 这里初始化ThreadLocalRandom类。
// 该类用于当前线程获取随机数。
// 并获取随机值h
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit();
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // 表示要放入的槽位是否发生碰撞
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
// 如果数组不为空
if ((as = counterCells) != null && (n = as.length) > 0) {
// 把随机数h映射到一个槽位,即获取一个随机槽位
// 若该槽位为空,则初始化该槽位
if ((a = as[(n - 1) & h]) == null) {
// 这里判断有没有上锁,如果上锁cellBusy是1
if (cellsBusy == 0) {
CounterCell r = new CounterCell(x); // x是新增的数量
// 尝试上锁cellsBusy
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try {
CounterCell[] rs; int m, j;
// 再次确认该槽位没有被初始化,否则可能把已初始化的值覆盖掉。
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break; // 初始化成功,退出循环
// 执行到这表示有多个线程执行了个这个分支。
// 且其他线程抢先初始化了这个槽位,
// 导致当前线程初始化这个槽位失败。
continue;
}
}
collide = false;
}
// 来到这则认为存在竞争
else if (!wasUncontended)
wasUncontended = true;
// 尝试通过cas更新猜槽位的值
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
// counterCells != as 表示数组被重组了。
else if (counterCells != as || n >= NCPU)
collide = false;
// 来到这里表示肯定发生了碰撞
else if (!collide)
collide = true;
// 这里尝试对数组上锁
// 若上锁成功将进行扩容操作
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {
// 扩容到原来的两倍
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0; // 解锁
}
collide = false;
continue;
}
// 获取新的随机码
h = ThreadLocalRandom.advanceProbe(h);
}
// 来到这表示数组还没被初始化
// 通过获取锁尽行初始化操作
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// 来到这表示获取锁失败,直接尝试增加到baseCount。
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
size方法
public int size() {
long n = sumCount(); // 获取计数值
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n); // 转化成int类型
}