java-JDK1.8 concurrenthashmap 源码阅读
本文部分照片和代码分析来自文末参考资料
java8中的concurrenthashmap的方法逻辑和注解有些问题,建议看最新的JDK版本
建议阅读 concurrenthashmap 源码前过一遍源码前面的注释,参考资料第二篇的博客有注释翻译
问题
- ConcurrentHashMap了解实现原理、扩容时做的优化、与HashTable对比。
- ConcurrentHashMap是如何在保证并发安全的同时提高性能?
- ConcurrentHashMap是如何让多线程同时参与扩容?
- 了解ConcurrentHashMap吗?说一下它为什么能线程安全
- size 方法是否阻塞 , get 方法刚好在迁移的时候是否也会阻塞 ?
概述
来自参考资料
说明:因为ConcurrentHashMap单词太长,所以下面均适用CHM替代ConcurrentHashMap
- 同为线程安全集合,但CHM没有任何访问操作需要锁定全表。这也注定了CHM上的操作效率之高。
- 表访问需要volatile/atomic读,写和CAS.这通过使用内在函数(sun.misc.Unsafe)完成。
- 向一个空bin中插入节点是通过CAS完成的,其它的更新操作(insert,delete,update)都需要锁lock。
- CHM的key,value都不能为null
- 访问操作(get等)和更新操作(remove等)同时发生时,根据happens-before原则,更新操作先执行,读操作后执行,从而保证了检索操作获取的值一定是最新值。
- 聚合状态方法的结果包括:size,isEmpty,containsValue通常都是仅当map不在其它线程中进行并发更新时才有用。
- 批量操作可以接受一个并行阈值参数parallelismThreshold。
- 如果当前map的size预估值比给定的阈值小,则方法顺序执行。
- 如果给定阈值=Long.MAX_VALUE,则不会出现并行操作。
- 如果给定阈值=1,则会导致并行最大化,通过使用ForkJoinPool.commonPool()方法,对子任务分离。
- 并行操作通常比顺序操作快,但不能保证一定是这样。并行操作更慢的情况有:
- 如果并行计算的基础工作比计算本身更昂贵,那么小map上的并行操作可能比顺序形式执行更慢。
- 如果所有处理器都忙于执行不相关的任务,并行化可能无法实现太多的实际并行性。(无法形成流水线操作)
- 支持序列化,不支持浅拷贝
- 两个线程访问同一个bin中不同的元素的锁争用概率为:1 / (8 * #elements)
- TreeBins存在的意义:保护了我们免于因过度resize带来的最坏影响。
- 每一个bin中的元素到达新的bin后要么索引不变,要么产生2的次幂的位移。我们通过捕获旧节点可以重用的情况来消除不必要的节点创建。平均而言,当table进行resize时,只有1/6的节点需要进行clone。
- table进行resize时,其它线程可以加入协助resize的过程(这不是为了获取锁),从而使得平均聚合等待时间变短。
- 在遇到转发节点时,遍历会移动到新table而无需重新访问节点
- TreeBins也需要额外的锁定机制。list更新过程中依旧可以进行遍历,但是红黑树在更新时却不能进行遍历,因为红黑树的调整可能会改变树的根节点,也可能改变各个节点之间的连接情况。
- TreeBins包含一个简单的读写锁定机制,依赖于主要的同步策略:
插入,删除的结构调整会调用lock机制; - 如果在结构调整前有读操作,则必须读操作完成后,再进行结构的调整操作。遵循happes-before原则。
扩展AbstractMap,但这只是仅仅为了与这个类的以前版本兼容。
能够用TreeMap替代TreeBin?
不能, 原因:TreeBins的查询及与查询相关的操作都使用了一种特殊的比较形式。TreeBins中包含的元素可能在实现Comparable上的原则不一样,所以对于它们之间的比较,则无法调用CompareTo()方法。为了解决这一问题,tree通过hash值对其排序。如果Comparable.compareTo 可用的话,再用这个方法对元素排序。在查找节点时,如果元素不具有可比性或比较为0,则可能需要对此节点对左右孩子都进行查询。如果所有元素都不是可比较的并且具有相同的哈希值,则需要对全table进行扫描。
从JDK8开始,CHM使用CAS算法替代了Segment的概念,保证线程安全。
-- 构成结构:bin数组+链表+红黑树
-- 红黑树被包装在TreeBin内
-- 扩容机制:2倍扩容
常量:
默认容量:16;
负载因子:0.75
说明:在构造函数中重写此值只会影响初始表的容量,而不会使用实际的浮点值。
链表转红黑树阈值:8
红黑树转链表阈值:6
table转为红黑树阈值:64
resize时的最小并行度:16(因为默认16个bin)
java8 的 concurrenthashmap 主要工作方式如下 :
可以看到总体就是链表+红黑树。当链表数量达到一定值(默认是8)时,我们就会将链表转化为红黑树。concurrenthash的特点就是并发,那么是如何并发的呢?下面是几个concurrenthash 的工作过程。
源码阅读
变量
/* ---------------- Fields -------------- */
/**
* 底层数组
*/
transient volatile Node<K,V>[] table;
//扩容时 使用的另一个数组
private transient volatile Node<K,V>[] nextTable;
//统计size的一部分
private transient volatile long baseCount;
/**
* sizeCtl与table的resize和init有关
* sizeCtl = -1时,表示table正在init
* sizeCtl < 0 且不等于-1时,表示正在resize
* sizeCtl > 0 时,表示下次需要resize的阈值,即capacity * loadfactory , 例如 初始化的时候 ,table 默认大小为 16,sizeCtl 被设置为 12
*/
private transient volatile int sizeCtl;
//记录下一次要transfer对应的Index
private transient volatile int transferIndex;
//表示是否有线程正在修改CounterCells
private transient volatile int cellsBusy;
//用来统计size
private transient volatile CounterCell[] counterCells;
// views
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
-
transferIndex : 指示目前迁移到哪了
-
stride : (这个是在transfer方法的局部变量,拿出来说明,是为了下文更好地理解transfer)假如一个迁移任务分为多个小迁移任务,一个位置为一个任务单位,小迁移任务中包含多个任务,stride代表小迁移任务中的任务数量。最小的stride是I6,最小的数组长度也是16,就意味着迁移任务分为1个小迁移任务,小迁移任务中有16个任务。
-
basecount : 统计数量的时候用到
-
cellsBusy : 基于 CAS 的自旋锁 ,当创建
CounterCells或调整大小的时候使用 -
CounterCell[] counterCells : 统计数量的时候用到
常量
/* ---------------- Constants -------------- */
/**
* HashMap的最大容量
*/
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 int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
* 负载因子
*/
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;
//这个常量是用来计算HashMap不同容量有不同的resizeStamp用的
private static int RESIZE_STAMP_BITS = 16;
//最大参与扩容的线程数 相当大的一个数 基本上是不会触及该上线的
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
//要对resizeStamp进行位移运算的一个敞亮
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
//特殊的节点哈希值
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
//获取CPU的数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
put 方法
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//计算 hash 值
int hash = spread(key.hashCode());
int binCount = 0;
//for 循环 , 除非遇到 break , 或是 return
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 首次加入或是 tab 里面没元素
if (tab == null || (n = tab.length) == 0)
tab = initTable(); //初始化 Table,然后继续运行,到for里面继续循环
//发现在列表中没有
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//CAS 放进去,成功后 break退出循环
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//在列表中存在,hash 值为 MOVED(-1)
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
//进到这里 f , fh 分别都被赋值了,列表中存在该值
V oldVal = null;
//直接加锁操作
synchronized (f) {
if (tabAt(tab, i) == f) {
//如果为链表 , 为什么 fh >= 0 是链表 ,是因为红黑树的根hash值为非正数
if (fh >= 0) {
binCount = 1;
// for 循环整个链表
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;
}
//找不到,e指向e后面那个
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) {
//存在一样的 key , 更新
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// binCount 表示这个槽位有的节点有多少个
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
//判断是否应该变为红黑树,treeifyBin 也有可能是扩展了数组大小。
// 也就是说每当有一个链表的数量大于 8 的时候就会进到这里来
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 总数 + 1
addCount(1L, binCount);
return null;
}
初始化数组
/**
* Initializes table, using the size recorded in sizeCtl.
*
* 这里假如有多个线程执行put 操作,那么初始化线程的操作应该只能执行一次
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//CAS 将 sizeCtl 赋值为 -1 ,那么CAS失败的线程进行循环要不让步,要不就tab得到赋值退出
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//这里就是 0.75*n
sc = n - (n >>> 2);
}
} finally {
// sizeCtl = sc = 0.75*n
sizeCtl = sc;
}
break;
}
}
return tab;
}
Thread.yield()方法的作用是让当前线程暂停执行,让出CPU资源给其他线程。调用Thread.yield()方法后,当前线程会进入就绪状态,等待CPU重新调度执行。
Thread.yield()方法并不是强制性的,它只是对线程调度器的一个建议,告诉线程调度器当前线程已经完成了一些重要的工作,可以让出CPU资源给其他线程执行。但是,线程调度器不一定会采纳这个建议,因此Thread.yield()方法并不能保证当前线程一定会让出CPU资源。
统计数量
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
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;
}
就是baseCount + 各数组之和 , 那么 CounterCell[] 到底是什么呢 ? 它的原理是什么呢 ? 从 CounterCell 这类上的注释上看到 :
该类改编自 LongAdder 和 Striped64 , 我们思考一下 ,我们统计数量其实也可以使用 AtomicLong 这样子的原子类去统计, 但是 concurrenthashmap
追求更高的性能 , 用 LongAdder 来实现 , LongAdder 的实现原理如下 :

这里就不深入源码去理解这个过程了.
扩容和迁移
假如上面的put 方法进入了treeifyBin 方法,那么接下来要进行扩容。
迁移有几个要点需要理解一下
- 迁移过程分为 : 1. 邻取迁移任务 2. 执行迁移任务
- 迁移过程从后往前 , 以最大限度避免遍历时冲突 , 遍历是从前往后遍历
- 当数组大小小于64 , 就先会先扩容迁移 ,而不是马上就变成红黑树
/**
* Replaces all linked nodes in bin at given index unless table is
* too small, in which case resizes instead.
*/
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n;
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;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
/**
* Tries to presize table to accommodate the given number of elements.
*
* @param size number of elements (doesn't need to be perfectly accurate)
*/
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;
//和initTable一样
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSetInt(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;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
//扩容迁移
int rs = resizeStamp(n);
if (U.compareAndSetInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
链表树化头结点会是一个特殊的 Node --- TreeBin , 这种node 里面存在着一个类似于读写锁的成员变量 , 所以它的 find 方法 有可能会阻塞 , 而它的 find方法在 concurrentHashMap 的 get 方法有所体现 .
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16
// stride 可以理解为”步长“,有 n 个位置是需要进行迁移的,
// 将这 n 个任务分为多个任务包,每个任务包有 stride 个任务
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 如果 nextTab 为 null,先进行一次初始化
// 前面我们说了,外围会保证第一个发起迁移的线程调用此方法时,参数 nextTab 为 null
// 之后参与迁移的线程调用此方法时,nextTab 不会为 null
if (nextTab == null) {
try {
// 容量翻倍
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 是 ConcurrentHashMap 中的属性
nextTable = nextTab;
// transferIndex 也是 ConcurrentHashMap 的属性,用于控制迁移的位置
transferIndex = n;
}
int nextn = nextTab.length;
// ForwardingNode 翻译过来就是正在被迁移的 Node
// 这个构造方法会生成一个Node,key、value 和 next 都为 null,关键是 hash 为 MOVED
// 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后,
// 就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了
// 所以它其实相当于是一个标志。
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 注意一个批次里面有多个槽位 , 这两个表示的东西不一样
// advance 指的是做完了一个槽位的迁移工作,是否可以进行下一个槽位的搬运
// finishing 指的是迁移是否已经完成
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
/*
* 下面这个 for 循环,最难理解的在前面,而要看懂它们,应该先看懂后面的,然后再倒回来看
*
*/
// i 是位置索引,bound 是边界,注意是从后往前
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 下面这个 while 真的是不好理解
// advance 为 true 表示可以进行下一个位置的迁移了
// 简单理解结局:i 指向了 transferIndex,bound 指向了 transferIndex-stride
// 下面这个while 的作用是 i 和 bound 进行 赋值 (为每个线程划定任务区)
while (advance) {
int nextIndex, nextBound;
/**
* --i >= bound 条件成立就表示进行下一个桶迁移 ,减一
* finishing 该批次迁移完成啦!!
*/
if (--i >= bound || finishing)
advance = false;
// 将 transferIndex 值赋给 nextIndex
// 这里 transferIndex 一旦小于等于 0,说明原数组的所有位置都有相应的线程去处理了
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//一次小迁移任务完成后(此时还有得分配),迁移的线程继续来到这里,和想帮忙的线程一起抢
else if (U.compareAndSetInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// 看括号中的代码,nextBound 是这次迁移任务的边界,注意,是从后往前
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
//抢不到就继续循环,直到退出
}
//这个if里面的作用是对所有的迁移操作是否完成进行判断 (判断所有任务是否 OK ),或是从 上面第二个 else if 过来的
/**
* n : 原数组槽位数
* nextn : 新数组槽位数
*
* 1. i < 0 最后一批次的位置,即原数组的第一个元素都迁移完了, 因为是从后往前迁移的,所以意味着结束迁移任务分配完成 ,(此时有可能还在搬运, 只是不需要其他线程协助搬运了); 也可能是上面 while 里面的第二个 `else if` i 被赋值了 -1
* 2. i >= n 代码A处 i = n ,表示迁移完成了
* 3. i + n >= nextn
*
*
* */
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
// 所有的迁移操作已经完成
nextTable = null;
// 将新的 nextTab 赋值给 table 属性,完成迁移
table = nextTab;
// 重新计算 sizeCtl:n 是原数组长度,所以 sizeCtl 得出的值将是新数组长度的 0.75 倍
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
// 然后,每有一个线程参与迁移就会将 sizeCtl 加 1,
// 这里使用 CAS 操作对 sizeCtl 进行减 1,代表做完了属于自己的任务
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
// 进来这里就是说明这一个批次的这个线程迁移完成,其他线程还在帮忙迁移没有完成 ,所以 return 就行了
//(假如一个次迁移完成,没人帮忙,等号两边应该是相等的)
return;
//最后一批迁移任务完成
// 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
// 也就是说,所有迁移线程都退出了(即是迁移任务完成了),也就会进入到上面的 if(finishing){} 分支了
finishing = advance = true;
i = n; // recheck before commit 代码A处
}
}
//下面的 else if 是对分到的任务区进行判断,是否已经迁移过了(是否迁移过的标志就是 ForwardingNode)
//要是都不是,那么就是还没迁移咯,开始自己任务区的迁移工作!!
// 如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 ForwardingNode ”空节点“,
//表明这个区域正在迁移
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 该位置处是一个 ForwardingNode,代表该位置已经迁移过了
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 对数组该位置处的结点加锁,开始处理数组该位置处的迁移工作
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 头结点的 hash 大于 0,说明是链表的 Node 节点 ? 这是为啥
//
if (fh >= 0) {
// 下面这一块和 Java7 中的 ConcurrentHashMap 迁移是差不多的,
// 需要将链表一分为二,
// 找到原链表中的 lastRun,然后 lastRun 及其之后的节点是一起进行迁移的
// lastRun 之前的节点需要进行克隆,然后分到两个链表中
// 文档中说到大概只有六分之一需要复制
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);
}
// 其中的一个链表放在新数组的位置 i
setTabAt(nextTab, i, ln);
// 另一个链表放在新数组的位置 i+n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
// 他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
setTabAt(tab, i, fwd);
// advance 设置为 true,代表该位置已经迁移完毕,进行下一个循环
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;
}
}
// 如果一分为二后,节点数少于 8,那么将红黑树转换回链表
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;
// 将 ln 放置在新数组的位置 i
setTabAt(nextTab, i, ln);
// 将 hn 放置在新数组的位置 i+n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
// 其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
setTabAt(tab, i, fwd);
// advance 设置为 true,代表该位置已经迁移完毕
advance = true;
}else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
}
}
}
sizeCtl 为负数的时候后16位被用作数据迁移的线程 .
/**
* Returns the stamp bits for resizing a table of size n.
* Must be negative when shifted left by RESIZE_STAMP_SHIFT.
*
* Integer.numberOfLeadingZeros(n) 返回的是 n 前面有多少个零
*
*/
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
这里可能对 sizeCtl 的移位判断难以理解, 下面我写了一段用例 , 应该就很好理解了
@Slf4j
public class ChashMapTest {
private static int RESIZE_STAMP_BITS = 16;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
public static void main(String[] args) {
for (int n = 1; n > 0; n *= 2) {
/**
* int rs = resizeStamp(n);
* int ans = (rs << RESIZE_STAMP_SHIFT) + 2;
*/
int nd = (n << 1) - (n >>> 1);
int rs = resizeStamp(n);
int sc =( rs << RESIZE_STAMP_SHIFT )+ 2;
String rsb = Integer.toBinaryString(resizeStamp(n));
String scb =Integer.toBinaryString(( rs << RESIZE_STAMP_SHIFT )+ 2);
log.info("n: {} , rs :{} , sc:{} , rsb :{} , scb:{} , nd:{}", n, rs, sc,rsb,scb,nd );
}
}
其中 rsb 和 scb是前面两个变量的二进制表示
n: 1 , rs :32799 , sc:-2145452030 , rsb :1000000000011111 , scb:10000000000111110000000000000010 , nd:2
n: 2 , rs :32798 , sc:-2145517566 , rsb :1000000000011110 , scb:10000000000111100000000000000010 , nd:3
n: 4 , rs :32797 , sc:-2145583102 , rsb :1000000000011101 , scb:10000000000111010000000000000010 , nd:6
n: 8 , rs :32796 , sc:-2145648638 , rsb :1000000000011100 , scb:10000000000111000000000000000010 , nd:12
n: 16 , rs :32795 , sc:-2145714174 , rsb :1000000000011011 , scb:10000000000110110000000000000010 , nd:24
n: 32 , rs :32794 , sc:-2145779710 , rsb :1000000000011010 , scb:10000000000110100000000000000010 , nd:48
n: 64 , rs :32793 , sc:-2145845246 , rsb :1000000000011001 , scb:10000000000110010000000000000010 , nd:96
n: 128 , rs :32792 , sc:-2145910782 , rsb :1000000000011000 , scb:10000000000110000000000000000010 , nd:192
n: 256 , rs :32791 , sc:-2145976318 , rsb :1000000000010111 , scb:10000000000101110000000000000010 , nd:384
n: 512 , rs :32790 , sc:-2146041854 , rsb :1000000000010110 , scb:10000000000101100000000000000010 , nd:768
n: 1024 , rs :32789 , sc:-2146107390 , rsb :1000000000010101 , scb:10000000000101010000000000000010 , nd:1536
n: 2048 , rs :32788 , sc:-2146172926 , rsb :1000000000010100 , scb:10000000000101000000000000000010 , nd:3072
n: 4096 , rs :32787 , sc:-2146238462 , rsb :1000000000010011 , scb:10000000000100110000000000000010 , nd:6144
n: 8192 , rs :32786 , sc:-2146303998 , rsb :1000000000010010 , scb:10000000000100100000000000000010 , nd:12288
n: 16384 , rs :32785 , sc:-2146369534 , rsb :1000000000010001 , scb:10000000000100010000000000000010 , nd:24576
n: 32768 , rs :32784 , sc:-2146435070 , rsb :1000000000010000 , scb:10000000000100000000000000000010 , nd:49152
n: 65536 , rs :32783 , sc:-2146500606 , rsb :1000000000001111 , scb:10000000000011110000000000000010 , nd:98304
n: 131072 , rs :32782 , sc:-2146566142 , rsb :1000000000001110 , scb:10000000000011100000000000000010 , nd:196608
n: 262144 , rs :32781 , sc:-2146631678 , rsb :1000000000001101 , scb:10000000000011010000000000000010 , nd:393216
n: 524288 , rs :32780 , sc:-2146697214 , rsb :1000000000001100 , scb:10000000000011000000000000000010 , nd:786432
n: 1048576 , rs :32779 , sc:-2146762750 , rsb :1000000000001011 , scb:10000000000010110000000000000010 , nd:1572864
n: 2097152 , rs :32778 , sc:-2146828286 , rsb :1000000000001010 , scb:10000000000010100000000000000010 , nd:3145728
n: 4194304 , rs :32777 , sc:-2146893822 , rsb :1000000000001001 , scb:10000000000010010000000000000010 , nd:6291456
可以看到 + 2 以后其实就是后两位变成 10了 ,或者这样看 :

更加形象一点我们用以下的图片来介绍迁移的过程
- 多线程开始扩容

-
lastrun节点
![img]()
-
链表迁移
![img]()
-
红黑树迁移
![img]()
-
迁移过程中get和put的操作的处理
![img]()
-
并发迁移

- 迁移完成

帮助迁移
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// sc == rs + 1 不知是什么意思
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
transferIndex <= 0)
break;
//到这里就表示条件满足了, CAS 后进入帮助迁移
//可以知道 nextTab 此时不为空了,要是进入是空的话,方法内部也会创建
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
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;
}
// eh < 0 ,如果是红黑树,去红黑树上查找,如果是 ForwardingNode ,则会跳到扩容后的 map 上寻找
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;
}
总结
文章介绍了 ConcurrentHashMap 的原理 , 不足之处请指出






浙公网安备 33010602011771号