一.ConcurrentHashMap数据结构
1.JDK1.7数据结构
JDK1.7中的HashMap的结构,ConcurrentHashMap将数组每个元素作为一个segment–片段。
结构:ReentrantLock+Segment+HashEntry
1.get操作,没有使用锁,而是通过Unsafe对象的getObjectVolatile()方法提供的原子读语义,来获得Segment以及对应的链表,然后对链表遍历判断是否存在key相同的节点以及获得该节点的value。但由于遍历过程中其他线程可能对链表结构做了调整,因此get和containsKey返回的可能是过时的数据,这一点是ConcurrentHashMap在弱一致性上的体现。
2.put操作,经过两次hash定位数据存储位置,第一次hash定位所在segment,如果segment没有赋值,先通过CAS操作确保初始化一个segment,在拿到segment对象后,会先尝试对整个segment加锁:
i.加锁成功,进行第二次hash定位HashEntry数组索引位,进行插入操作,而后唤醒其他堵塞线程
ii.加锁失败,在多处理器中自旋重试最多64次尝试获取锁,仍然失败调用lock进入堵塞,等待唤醒。
3.size操作,遍历每个segment的元素数量累加,先尝试不加锁获取2次,如果2次获取过程中容器count发生变化,再给每个Segment进行加锁,重新计算一次元素的个数。
2.JDK1.8数据结构
结构:synchronized+CAS+HashEntry+红黑树
对于各类操作,实现原理如下:
1.get操作,不加锁,先根据key的hash值定位数组元素位置,而后根据链表或红黑树查找节点。
2.put操作,如果数组对应hash位未初始化,先尝试用cas插入数据,如果已经初始化或插入失败,则对该节点加synchronized锁,而后基于链表或红黑树插入算法插入新元素。当链表大于一定长度,会进行扩容或红黑树化。
3.1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount变量和counterCells数组。size方法通过sumCount变量baseCount变量和counterCells数组拿到最终map的大小。
对比:
- JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
- JDK1.8版本的数据结构变得更加简单
- 1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程
疑问:JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock?
答:1.synchronized在低粒度下,性能表现不亚于ReentrantLock 2.在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存
衡量一个好的数据类型工具从两方面考虑:
1.更好使用的api(即方法 如获取map大小方式)
2.更好的性能(从数据结构上优化(红黑树、单数组)、使用的更合适并发工具(synchronized + cas)建立在降低锁力度)
3.ConcurrentHashMap源码核心实现
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
// 表的最大容量,最高两位用于作控制位
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认表的大小
private static final int DEFAULT_CAPACITY = 16;
// 最大数组大小
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 装载因子
private static final float LOAD_FACTOR = 0.75f;
// 转化为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 由红黑树转化为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 转化为红黑树的表的最小容量,如果小于当前容量,优先考虑进行扩容而非树化
static final int MIN_TREEIFY_CAPACITY = 64;
// 每次进行转移的最小值,用于扩容操作
private static final int MIN_TRANSFER_STRIDE = 16;
// 生成sizeCtl所使用的bit位数
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;
// 节点哈希域编码
static final int MOVED = -1; // 表示节点正在转移
static final int TREEBIN = -2; // 表示已经转换成树
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
//
/** Number of CPUS, to place bounds on some sizings */
// 获取可用的CPU个数
static final int NCPU = Runtime.getRuntime().availableProcessors();
// 顶层数组
transient volatile Node<K,V>[] table;
// 扩容时使用,只有在扩容时才不为空
private transient volatile Node<K,V>[] nextTable;
// 在无竞争情况下的Map容量计数
private transient volatile long baseCount;
// 表初始化和扩容的状态控制位。
// -1为初始化,如果为其他负数,高16位标识当前扩容状态,低16位-n表示有n-1个线程正在进行扩容操作
// 当table = null,0为默认值,正数表示存储表格的容量
// 初始化完成后,表示table的容量,默认是table大小的0.75倍
private transient volatile int sizeCtl;
// 扩容下另一个表的索引
private transient volatile int transferIndex;
// 旋转锁,在扩容或创建CounterCells时使用
private transient volatile int cellsBusy;
// counterCell表,非空时,大小时2的幂次方
private transient volatile CounterCell[] counterCells;
}
Node和TreeNode内部类存储健值对成员
// 链表节点
static class Node<K,V> implements Map.Entry<K,V> {
// 哈希值
final int hash;
// 键
final K key;
// 值,需要保证可见性
volatile V val;
// 链表下一元素,需要保证可见性
volatile Node<K,V> next;
}
// 树非首节点
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;
}
// 树首节点,hash值固定为-2
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
}
// 一个特殊的Node节点,hash值为-1,其中存储nextTable的引用。
// 只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动。
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
}
在操作tab数组元素时,在ConcurrentHashMap中使用了unSafe方法,基于硬件的安全机制,通过直接操作内存的方式来保证并发处理的安全性。
/*
* 用来返回节点数组的指定位置的节点的原子操作
*/
@SuppressWarnings("unchecked")
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
/*
* cas原子操作,在指定位置设定值
*/
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
/*
* 原子操作,在指定位置设定值
*/
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
4.核心方法实现
put插入操作底层调用了putVal方法,传入了onlyIfAbsent=false,代表即使元素存在也进行更新插入。
putVal的核心实现逻辑大致如下:
1.先判断table是否已初始化,如果未初始化,先调用initTable函数进行初始化
2.否则根据hash定位元素所处数组位置,如果这个位置没有元素,先尝试通过无锁cas操作,操作成功退出循环。
3.根据hash定位元素所处数组位置,且位置节点不为空,判断当前节点是否处于转移扩容状态,如果是,则调用helpTransfer参与协助节点扩容过程。
4.定位到节点不为空,且非扩容状态,先进行加锁操作,而后根据节点为链表或树节点进行插入/更新操作,最后判断如果链表节点数量大于阈值,则进行树转化,完成后退出循环。
5.退出循环后根据操作过程的节点数量变化,调用addCount更新容器节点数量标识。在addCount函数内部,判定如果容量超过当前扩容阈值,会调用transfer方法进行扩容操作
1.put源码实现
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key & value 不能为null
if (key == null || value == null) throw new NullPointerException();
// 计算hash值,计算方法:(h ^ (h >>> 16)) & HASH_BITS
int hash = spread(key.hashCode());
//用来计算在这个节点总共有多少个元素,用来控制扩容或者转移为树
int binCount = 0;
// 循环操作,直到插入或更新操作成功
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 在第一次进行put操作时,会对表进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) //创建一个Node添加到数组中,null表示的是下一个节点为空
// 如果成功,说明插入成功,退出操作。
break;
}
// 检测当前数组位置首元素节点是否处于转移状态,表示正在进行数组扩张的数据复制阶段
else if ((fh = f.hash) == MOVED)
// 当前线程参与复制,通过允许多线程复制的功能,以此来减少数组的复制所带来的性能损失
tab = helpTransfer(tab, f);
else {
/*
* 如果在这个位置有元素的话,就采用synchronized的方式加锁,
* 如果是链表的话(hash大于0),就对这个链表的所有元素进行遍历,
* 如果找到了key和key的hash值都一样的节点,则把它的值替换到
* 如果没找到的话,则添加在链表的最后面
* 否则,是树的话,则调用putTreeVal方法添加到树中去
*
* 在添加完之后,会对该节点上关联的的数目进行判断,
* 如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者是扩容
*/
V oldVal = null;
// 锁表头元素
synchronized (f) {
// 多线程操作下,如果当前表头元素发生变化,要重新循环,不走一下分支逻辑
if (tabAt(tab, i) == f) {
if (fh >= 0) { // 当转换为树之后,hash值为-2, 这里取出来的元素的hash值大于0,说明为正常链表结果
// 记录首元素数量
binCount = 1;
for (Node<K,V> e = f;; ++binCount) { // 遍历链表
K ek;
// 遍历相等条件判定,插入key和原有key的地址或hash值相等。
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent) //当使用putIfAbsent的时候,只有在这个key没有设置值得时候才设置
e.val = value;
// 操作完成,退出循环
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
// 走到这里说明走到链表尾部,仍没有key相等节点,接下来直接插入新节点
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操作是否更新。
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 超过阈值8,尝试树转化
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); //计数,1L表示元素加1,binCount < 0表示不检查是否进行扩容, <= 1表示只在无冲突情况下检查
return null;
}
2.initTable 表格初始化操作
初始化操作开始会考虑到多线程竞争,确保只有一个线程进行初始化操作,其他让步等待初始化完成结束函数调用。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) { // 循环判定直至初始化成功
if ((sc = sizeCtl) < 0) // sizeCtl小于0,说明当前操作存在并发,已经有其他线程进行初始化或扩容操作,当前线程让步
Thread.yield();
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // cas更新为初始化状态,如果更新成功,则当前线程完成初始化操作
try {
if ((tab = table) == null || tab.length == 0) { // 二次检测
// sc的值是否大于0,若是,则n为sc,表示要初始化的tab大小,否则,n为默认初始容量
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// 新生结点数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 赋值给table
table = tab = nt;
// sc为n * 3/4
sc = n - (n >>> 2);
}
} finally {
// 设置sizeCtl的值
sizeCtl = sc;
}
break;
}
}
// 返回table表
return tab;
}
3.addCount 增加元素数量计数操作
对于这里,有几个特殊处理:
1.如果在cas更新过程中,存在并发冲突,考虑到可能走到这里,完成插入操作是相同key的元素,即存在并发冲突,需要特殊处理。
2.如果check<0,一般是移除操作,不用检查扩容,如果<=1,可能是初始化、插入/更新元素首节点,这个情况下,无冲突才去检查是否需要扩容
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// counterCells 不为空或者cas baseCount进行+x操作失败,都说明存在并发竞争
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
// 记录是否冲突
boolean uncontended = true;
// 在CounterCells未初始化,
// CounterCells随机位为null
// 或尝试通过CAS更新当前线程的CounterCell失败(同时设置uncontended为更新成功|失败,即无冲突|有冲突)
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 多线程修改baseCount时,竞争失败的线程会执行fullAddCount(x, uncontended),把x的值插入到counterCell类中
fullAddCount(x, uncontended);
return;
}
// 走到这,说明有冲突,check <= 1 则不检查
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) {
// 根据当前table长度计算rs,计算方法是Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)),
int rs = resizeStamp(n);
if (sc < 0) { // sc = sizeCtl小于0 说明有线程正在扩容
// 如果 sc 的高 16 位不等于 标识符(校验异常 sizeCtl 变化了)
// 条件2、3正常情况应该不可能走到?
// 如果 nextTable == null(结束扩容了)
// 如果 transferIndex <= 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);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null); // 仅当前线程在扩容
// 更新最新总量
s = sumCount();
}
}
}
在对baseCount cas操作失败后,会随机操作CounterCell数组某一位进行cas累加来分散并发冲突,如果还是失败,会进入到fullAddCount方法完成累加,而非通过cas自旋,以此进一步优化性能。
4.CounterCell冲突统计
CounterCell是定义在ConcurrentHashMap内的一个静态final内部类
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
CounterCell通过使用jdk1.8引入的新注解@sun.misc.Contended来避免伪缓存的问题。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,在多线程操作时,每个线程都要去竞争缓存行的所有权来更新变量,这就是伪共享,在Java7前,通过在前后属性paading 无用的long型变量补齐,在Java 8中,提供了@sun.misc.Contended注解来避免伪共享,原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,使用2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突。
CounterCell内部类是用来计数的 只有一个属性value ConcurrentHashMap通过最后调用sumCount方法汇总一个CounterCell数组所有CounterCell的值来实现计数,sumCount的计算主要是累加baseCount和counterCells中所有元素的数量:
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;
}
在cas冲突后,会调用fullAddCount来处理冲突,源码实现如下:
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
// 如果随机数为0,则当前线程强制初始化并发随机数
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
// 更新为无冲突
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
// counterCells数组不为空 且数组长度大于0
if ((as = counterCells) != null && (n = as.length) > 0) {
// 当前线程对应的数组元素为空
if ((a = as[(n - 1) & h]) == null) {
// 判断cellsBusy变量为0,也就是没有线程操作counterCells数组
if (cellsBusy == 0) { // Try to attach new Cell
// 初始化新的cell
CounterCell r = new CounterCell(x); // Optimistic create
// 更新当前cellBusy为占用状态,cas锁
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// 是否创建操作成功标识
boolean created = false;
try { // Recheck under lock
// 二次检查并尝试初始化
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;
}
// 状态成功,退出,否则继续循环cas创建
if (created)
break;
continue; // Slot is now non-empty
}
}
// 代表没有发生冲突的情况
collide = false;
}
// 判断如果上述不成立且counterCells数组该线程对应处元素不为空 且wasUncontended为false 代表之前发生过竞争 cas操作失败 将这个变量置为true 继续循环
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 如果上述不成立,则cas给对应cell的数量+1,成功则退出
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
// 如果上述不成立且counterCells和as不一致 或者 这个数组的长度大于等于CPU数目 collide设置为false 代表没有冲突了
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
// 如果上述不成立且collide还是false 那就将collide设为true 因为上述都不成立肯定发生冲突了
else if (!collide)
collide = true;
// 如果上述不成立且cellsBusy为0且调用Unsafe的cas方法将cellsBusy设为1的操作成功 就是说counterCells可以由本线程操作 就将counterCells数组扩容为原来的两倍 并继续循环
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
// 扩容
if (counterCells == as) {// Expand table unless stale
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; // Retry with expanded table
}
// 判断counterCells数组不为空的最后 h重新计算随机值
h = ThreadLocalRandom.advanceProbe(h);
}
// 上述条件不满足,说明counterCells数组为空,如果cellsBusy为0 且调用Unsafe的cas方法将cellsBusy成功设为1(加锁)成功 就创建一个长度为2的CounterCell数组 并在该线程对应的下标处创建一个新的CounterCell
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
}
}
扩容操作的核心在于数据的转移,在单线程环境下数据的转移很简单,无非就是把旧数组中的数据迁移到新的数组。但是这在多线程环境下是行不通的,需要保证线程安全性,在扩容的时候其他线程也可能正在添加元素,这时又触发了扩容怎么办?有人可能会说,这不难啊,用一个互斥锁把数据转移操作的过程锁住不就好了?这确实是一种可行的解决方法,但同样也会带来极差的吞吐量。
transfer()函数可以大致分为三部分,第一部分对后续需要使用的变量进行初始化:
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 根据当前机器的CPU数量来决定每个线程负责的bucket数
// 避免因为扩容线程过多,反而影响到性能
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 初始化nextTab,容量为旧数组的一倍
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 = nextTab;
transferIndex = n; // 初始化指针
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
第二部分为当前线程分配任务和控制当前线程的任务进度,这部分是transfer()的核心逻辑,描述了如何与其他线程协同工作:
// i指向当前bucket,bound表示当前线程所负责的bucket区域的边界
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 这个循环使用CAS不断尝试为当前线程分配任务
// 直到分配成功或任务队列已经被全部分配完毕
// 如果当前线程已经被分配过bucket区域
// 那么会通过--i指向下一个待处理bucket然后退出该循环
while (advance) {
int nextIndex, nextBound;
// --i表示将i指向下一个待处理的bucket
// 如果--i >= bound,代表当前线程已经分配过bucket区域
// 并且还留有未处理的bucket
if (--i >= bound || finishing)
advance = false;
// transferIndex指针 <= 0 表示所有bucket已经被分配完毕
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 移动transferIndex指针
// 为当前线程设置所负责的bucket区域的范围
// i指向该范围的第一个bucket,注意i是逆向遍历的
// 这个范围为(bound, i),i是该区域最后一个bucket,遍历顺序是逆向的
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 当前线程已经处理完了所负责的所有bucket
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 如果任务队列已经全部完成
if (finishing) {
nextTable = null;
table = nextTab;
// 设置新的阈值
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 工作中的扩容线程数量减1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// (resizeStamp << RESIZE_STAMP_SHIFT) + 2代表当前有一个扩容线程
// 相对的,(sc - 2) != resizeStamp << RESIZE_STAMP_SHIFT
// 表示当前还有其他线程正在进行扩容,所以直接返回
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 否则,当前线程就是最后一个进行扩容的线程
// 设置finishing标识
finishing = advance = true;
i = n; // recheck before commit
}
}
// 如果待处理bucket是空的
// 那么插入ForwardingNode,以通知其他线程
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 如果待处理bucket的头节点是ForwardingNode
// 说明此bucket已经被处理过了,跳过该bucket
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
最后一部分是具体的迁移过程(对当前指向的bucket),这部分的逻辑与HashMap类似,拿旧数组的容量当做一个掩码,然后与节点的hash进行与操作,可以得出该节点的新增有效位,如果新增有效位为0就放入一个链表A,如果为1就放入另一个链表B,链表A在新数组中的位置不变(跟在旧数组的索引一致),链表B在新数组中的位置为原索引加上旧数组容量。
else {
// 对于节点的操作还是要加上锁的
// 不过这个锁的粒度很小,只锁住了bucket的头节点
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// hash code不为负,代表这是条链表
if (fh >= 0) {
// fh & n 获得hash code的新增有效位,用于将链表分离成两类
// 要么是0要么是1,关于这个位运算的更多细节
// 请看本文中有关HashMap扩容操作的解释
int runBit = fh & n;
Node<K,V> lastRun = f;
// 这个循环用于记录最后一段连续的同一类节点
// 这个类别是通过fh & n来区分的
// 这段连续的同类节点直接被复用,不会产生额外的复制
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
// 0被放入ln链表,1被放入hn链表
// lastRun是连续同类节点的起始节点
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
// 将最后一段的连续同类节点之前的节点按类别复制到ln或hn
// 链表的插入方向是往头部插入的,Node构造函数的第四个参数是next
// 所以就算遇到类别与lastRun一致的节点也只会被插入到头部
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链表被放入到原索引位置,hn放入到原索引 + 旧数组容量
// 这一点与HashMap一致,如果看不懂请去参考本文对HashMap扩容的讲解
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd); // 标记该bucket已被处理
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) {
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;
}
}
// 元素数量没有超过UNTREEIFY_THRESHOLD,退化成链表
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;
}
4.计数
ConcurrentHashMap的计数设计在一个低并发的情况下,就只是简单地使用CAS操作来对baseCount进行更新,但只要这个CAS操作失败一次,就代表有多个线程正在竞争,那么就转而使用CounterCell数组进行计数,数组内的每个ConuterCell都是一个独立的计数单元。
每个线程都会通过ThreadLocalRandom.getProbe() & m寻址找到属于它的CounterCell,然后进行计数。ThreadLocalRandom是一个线程私有的随机数生成器,每个线程的probe都是不同的,可以认为每个线程的probe就是它在CounterCell数组中的hash code。这种方法将竞争数据按照线程的粒度进行分离,相比所有竞争线程对一个共享变量使用CAS不断尝试在性能上要效率多。
fullAddCount()函数根据当前线程的probe寻找对应的CounterCell进行计数,如果CounterCell数组未被初始化,则初始化CounterCell数组和CounterCell。把CounterCell数组当成一个散列表,每个线程的probe就是hash code,散列函数是(n - 1) & probe。
CounterCell数组的大小永远是一个2的n次方,初始容量为2,每次扩容的新容量都是之前容量乘以二,处于性能考虑,它的最大容量上限是机器的CPU数量。
所以说CounterCell数组的碰撞冲突是很严重的,因为它的bucket基数太小了。而发生碰撞就代表着一个CounterCell会被多个线程竞争,为了解决这个问题,Doug Lea使用无限循环加上CAS来模拟出一个自旋锁来保证线程安全,自旋锁的实现基于一个被volatile修饰的整数变量cellsBusy,该变量只会有两种状态:0和1,当它被设置为0时表示没有加锁,当它被设置为1时表示已被其他线程加锁。这个自旋锁用于保护初始化CounterCell、初始化CounterCell数组以及对CounterCell数组进行扩容时的安全。
CounterCell更新计数是依赖于CAS的,每次循环都会尝试通过CAS进行更新,如果成功就退出无限循环,否则就调用ThreadLocalRandom.advanceProbe()函数为当前线程更新probe,然后重新开始循环,以期望下一次寻址到的CounterCell没有被其他线程竞争。
如果连着两次CAS更新都没有成功,那么会对CounterCell数组进行一次扩容,这个扩容操作只会在当前循环中触发一次,而且只能在容量小于上限时触发。
总结大致如下:先尝试给baseCount添加,如果并发冲突过于激烈,这里避免自旋产生过多性能损耗,会将计数分散到一个CounterCell数组中,通过并发随机数更新数组某一位元素,不存在则初始化,存在则加1,如果竞争依然过于激烈,会尝试对CounterCells数组进行扩容,进一步分散冲突。这里会存在cas操作,但相对直接更新baseCount,冲突的可能性被分散,性能会优化很多。
5.resizeStamp 扩容标识计算
从源码中我们看到,在调用transfer扩容前,会有调用resizeStamp生成一个rs来不断和sizeCtl比较判断扩容状态。resizeStamp相关的几个参数和方法实现如下:
/**
* 基于sizeCtl生成扩容戳的位数。
*/
private static int RESIZE_STAMP_BITS = 16;
/**
* 在sizeCtl中记录容量的位偏移
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
/**
* 计算扩容戳
* 计算数组长度的前置0个数,如2^10的前置0个数位21个,
* 计算出前置0个数后和2^15异或,即加上2^15,返回计算结果
*/
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
即sizeCtl的具体意义位:
高RESIZE_STAMP_BITS位:扩容标记,最高位1表示负数,其余位表示n的前置0个数,作为这次扩容的标记
低RESIZE_STAMP_SHIFT位:并行扩容线程数
后续对于sizeCtl的主要操作有:
如果多一个线程加入协助扩容,则sizeCtl+1
在transfer里,如果当前线程扩容完成,则sizeCtl-1
6.transfer容器容量扩容操作
在通过s=sumCount计算出当前元素总量后,会和sizeCtl进行比较,sizeCtl如果为负数,说明为扩容中,又或者为正数,为当前容量的0.75,当s>sizeCtl,说明需要进行扩容了,会进一步分析以下两种情况参与扩容:
sc < 0: 当前有其他其他线程进行扩容,则尝试加入协助扩容,调用transfer(tab, nt)
更新sc为(rs << RESIZE_STAMP_SHIFT) + 2成功,相当于设置sc为启动扩容标识,可以开始作为初始化线程启动扩容,调用transfer(tab, null)
具体扩容函数实现源码较为复杂,下面先看看大致扩容流程:
对于启动扩容的线程,会首先初始化一个2倍长的数组,更新成员属性nextTable为新数组,transferIndex=n,表示当前扩容操作的槽位,这是一个violate变量,会被多线程读和cas并发修改,多线程并发扩容根据transferIndex来定位操作特定数组元素。
初始化一个步长stride,每个线程会根据transferIndex和stride,认领table从(transferIndex-stride)~stransferIndex位置的节点进行扩容,并用i来表示当前扩容的元素节点位,同时更新transferIndex=transferIndex-stride
每个线程操作认领区间链表,如果节点是null,则更新为ForwardingNode,如果当前为ForwardingNode,说明已被操作,则跳过。其余为链表节点或树节点进行扩容,扩容的具体流程大致是将原来链表拆分成两条链表或两棵树,并插入新链表的当前位置i和n+i位置,这里因为hash算法特性,不会存在线程安全问题。
当操作完所属区间,如果仍有区间未扩容完,继续走第三步流程
扩容完后,更新finished=true,i=n,再整个链表完整check一次,进行查漏补缺。
二次check完成后,扩容结束。