概述
关于Java集合的小抄是这么描述:
-
并发优化的HashMap。
-
在JDK5里的经典设计,默认16把写锁(可以设置更多),有效分散了阻塞的概率。数据结构为Segment[],每个Segment一把锁。Segment里面才是哈希桶数组。Key先算出它在哪个Segment里,再去算它在哪个哈希桶里。
-
也没有读锁,因为put/remove动作是个原子动作(比如put的整个过程是一个对数组元素/Entry 指针的赋值操作),读操作不会看到一个更新动作的中间状态。
-
但在JDK8里,Segment[]的设计被抛弃了,改为精心设计的,只在需要锁的时候加锁。
-
支持ConcurrentMap接口,如putIfAbsent(key,value)与相反的replace(key,value)与以及实现CAS的replace(key, oldValue, newValue)。
源码分析
成员变量
-
table:默认为null,初始化发生在第一次插入操作,默认大小为16的数组,用来存储Node节点数据,扩容时大小总是2的幂次方。
-
nextTable:默认为null,扩容时新生成的数组,其大小为原数组的两倍。
-
sizeCtl :默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。
等于-1时,代表 table 正在初始化
等于-N时,表示有N-1个线程正在进行扩容操作
如果table未初始化,表示table需要初始化的大小。
如果table初始化完成,表示table的容量,默认是table大小的0.75倍,居然用这个公式算0.75(n - (n >>> 2))。
- Node:保存key,value及key的hash值的数据结构。 其中value和next都用volatile修饰,保证并发的可见性。
class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
//... 省略部分代码
}
- ForwardingNode:一个特殊的Node节点,hash值为-1,其中存储nextTable的引用。 只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动。
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;
}
}
初始化数组
初始化数组的时候需要判断是否有其他线程正在执行初始化,采用CAS操作更新 sizeCtl 的值。具体代码如下:
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//如果一个线程发现sizeCtl<0,意味着另外的线程执行CAS操作成功正在初始化表,当前线程只需要让出cpu时间片
if ((sc = sizeCtl) < 0)
Thread.yield();
//通过 cas 操作,将 sizeCtl 替换为-1,标识当前线程抢占到了初始化资格
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
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;
//计算下次扩容的大小,实际就是当前容量的 0.75 倍,这里使用了右移来计算
sc = n - (n >>> 2);
}
} finally {
//设置sizeCtl为sc, 如果默认是16的话,那么这个时候 sc=16*0.75=12
sizeCtl = sc;
}
break;
}
}
return tab;
}
put函数
与 HashMap 的 put 操作类似,主要增加多线程情况的判断。具体实现如下:
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
//这里其实就是自旋操作,当出现线程竞争时不断自旋
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果tab为空则初始化table
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//通过 hash 值对应的数组下标得到第一个节点; 以 volatile 读的方式来读取 table 数组中的元素,保证每次拿到的数据都是最新的
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//如果该下标返回的节点为空,则直接通过 cas 将新的值封装成 node 插入即可;如果 cas 失败,说明存在竞争,则进入下一次循环
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 当前hash为MOVED表示Map在扩容,先协助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
//hash冲突,通过 synchronized 加锁当前位置对象防止多线程更新
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;
}
}
}
//链表节点 >= 8个则已经转换为红黑树,遍历寻找进行替换值或新增节点
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) {
//如果链表节点个数 >= 8,则转换为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//将当前 ConcurrentHashMap 的元素数量加 1,有可能触发 transfer 操作(扩容)
addCount(1L, binCount);
return null;
}
tabAt
该方法获取对象中 offset 偏移地址对应的对象 field 的值。实际上这段代码的含义等价于 tab[i], 但是为什么不直接使用 tab[i]来计算呢?
getObjectVolatile,一旦看到 volatile 关键字,就表示可见性。因为对 volatile 写操作 happen- before 于 volatile 读操作,因此其他线程对 table 的修改均对 get 读取可见;虽然 table 数组本身是增加了 volatile 属性,但是“volatile 的数组只针对数组的引用具有 volatile 的语义,而不是它的元素”。所以如果有其他线程对这个数组的元素进行写操作,那么当前线程来读的时候不一定能读到最新的值。
出于性能考虑,Doug Lea 直接通过 Unsafe 类来对 table 进行操作。
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);
}
size
为了更好地统计size,ConcurrentHashMap提供了baseCount、counterCells两个辅助变量和一个CounterCell辅助内部类。
ConcurrentHashMap 是并发集合,如果用一个成员变量来统计元素个数的话,为了保证并发情况下共享变量的的及时更新,势必会需要通过加锁或者自旋 CAS 来实现,如果竞争比较激烈的情况下,size 的设置上会出现比较大的冲突反而影响了性能,所以在 ConcurrentHashMap 采用了分片的方法来记录大小。
// 标识当前 cell 数组是否在初始化或扩容中的 CAS 标志位
private transient volatile int cellsBusy;
//counterCells数组,总数值的分值分别存在每个 cell 中
private transient volatile CounterCell[] counterCells;
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
//ConcurrentHashMap中元素个数,但返回的不一定是当前Map的真实元素个数。基于CAS无锁更新
private transient volatile long baseCount;
CounterCell 数组的每个元素,都存储一个元素个数,而实际我们调用 size 方法就是通过这个循环累加来得到的。
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
//迭代counterCells来统计sum的过程
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;//所有counter的值求和
}
}
return sum;
}
数组扩容
当ConcurrentHashMap中table元素个数达到了容量阈值(sizeCtl)时,则需要进行扩容操作。在put操作时最后一个会调用addCount(long x, int check),该方法主要做两个工作:
1.更新 baseCount;
2.检测是否需要扩容操作。
fullAddCount
我们首先来看下是如何通过 counterCells 记录数组大小
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 判断 counterCells 是否为空,
//如果为空,就通过 cas 操作尝试修改 baseCount 变量,对这个变量进行原子累加操作(做这个操作的意义是:如果在没有竞争的情况下,仍然采用 baseCount 来记录元素个数)
//如果 cas 失败说明存在竞争,这个时候不能再采用 baseCount 来累加,而是通过 CounterCell 来记录
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
/**
* 这里有几个判断
* 1. 计数表为空则直接调用 fullAddCount
* 2. 从计数表中随机取出一个数组的位置为空,直接调用 fullAddCount
* 3. 通过 CAS 修改 CounterCell 随机位置的值,如果修改失败说明出现并发情况(这里又用到了一种巧妙的方法),调用 fullAndCount
* Random 在线程并发的时候会有性能问题以及可能会产生相同的随机 数,ThreadLocalRandom.getProbe 可以解决这个问题,并且性能要比 Random 高
*/
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 多线程CAS发生失败时执行
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
}
fullAddCount 主要是用来初始化 CounterCell,来记录元素个数,里面包含扩容,初始化等操作
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
//获取当前线程的 probe 的值,如果值为 0,则初始化当前线程的 probe 的值,probe 就是随机数
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;// 由于重新生成了probe,未冲突标志位设置为true
}
boolean collide = false; // True if last slot nonempty
for(;;) {//自旋
CounterCell[] as;
CounterCell a;
int n;
long v;
//说明 counterCells 已经被初始化过了
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {// 通过该值与当前线程 probe 求与,获得 cells 的下标元素,和 hash 表获取索引是一样的
if (cellsBusy == 0) { //cellsBusy=0 表示 counterCells 不在初始化或者扩容状态下
CounterCell r = new CounterCell(x); //构造一个CounterCell的值,传入元素个数
if (cellsBusy == 0 &&//通过cas设置cellsBusy标识,防止其他线程来 对 counterCells 并发处理
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
CounterCell[] rs;
int m, j;
//将初始化的 r 对象的元素个数放在对应下标的位置
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;//说明指定 cells 下标位置的数据不为空,则进行下一次循环
}
}
collide = false;
}
//说明在 addCount 方法中 cas 失败了,并且获取 probe 的值不为空
else if (!wasUncontended)
wasUncontended = true; //设置为未冲突标识,进入下一次自旋
//由于指定下标位置的 cell 值不为空,则直接通过 cas 进行原子累加,如果成功,则直接退出
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
//如果已经有其他线程建立了新的 counterCells 或者 CounterCells 大于 CPU 核心数 (很巧妙,线程的并发数不会超过 cpu 核心数)
else if (counterCells != as || n >= NCPU)
collide = false; //设置当前线程的循环失败不进行扩容
else if (!collide)//恢复collide状态,标识下次循环会进行扩容
collide = true;
//进入这个步骤,说明 CounterCell 数组容量不够,线程竞争较大,所以先设置一个标识表示为正在扩容
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
//扩容一倍 2 变成 4,这个扩容比较简单
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);//更新随机数的值
}
//cellsBusy=0 表示没有在做初始化,通过 cas 更新 cellsbusy 的值标注当前线程正在做初始化操作
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]; //初始化容量为 2
rs[h & 1] = new CounterCell(x);//将 x 也就是元素的个数放在指定的数组
下标位置
counterCells = rs;//赋值给counterCells
init = true;//设置初始化完成标识
}
} finally {
cellsBusy = 0;//恢复标识
}
if (init)
break;
}
//竞争激烈,其它线程占据 cell 数组,直接累加在 base 变量中
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break;
}
}
transfer
其次我们来看下关于数组扩容的代码
/**
* 判断是否需要扩容,也就是当更新后的键值对总数 baseCount >= 阈值 sizeCtl 时,进行rehash,这里面会有两个逻辑。
* 1. 如果当前正在处于扩容阶段,则当前线程会加入并且协助扩容
* 2. 如果当前没有在扩容,则直接触发扩容操作
*/
if (check >= 0) {//如果 binCount>=0,标识需要检查扩容
Node<K,V>[] tab, nt; int n, sc;
//s 标识集合大小,如果集合大小大于或等于扩容阈值(默认值的 0.75)并且 table 不为空并且 table 的长度小于最大容量则进行扩容
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);//这里是生成一个唯一的扩容戳
//sc<0,也就是sizeCtl<0,说明已经有别的线程正在扩容了
if (sc < 0) {
/**
* 这 5 个条件只要有一个条件为 true,说明当前线程不能帮助进行此次的扩容,直接跳出循环
* 1. sc >>> RESIZE_STAMP_SHIFT!=rs 表示比较高 RESIZE_STAMP_BITS 位生成戳和 rs 是否相等,判断是否同一次扩容
* 2. sc=rs+1 表示扩容结束
* 3. sc==rs+MAX_RESIZERS 表示帮助线程线程已经达到最大值了
* 4. nt=nextTable -> 表示扩容已经结束
* 5. transferIndex<=0 表示所有的 transfer 任务都被领取完了,没有剩余的 hash 桶来给自己自己好这个线程来做 transfer
*/
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//当前线程尝试帮助此次扩容,如果成功,则调用 transfer
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 如果当前没有在扩容,那么 rs 肯定是一个正数,通过 rs<<RESIZE_STAMP_SHIFT 将 sc 设置 为一个负数,+2 表示有一个线程在执行扩容
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();// 重新计数,判断是否需要开启下一轮扩容
}
}
resizeStamp 用来生成一个和扩容有关的扩容戳,具体有什么作用呢?我们基于它的实现来 做一个分析
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
Integer.numberOfLeadingZeros 这个方法是返回无符号整数 n 最高位非 0 位前面的 0 的个 数
比如 10 的二进制是 0000 0000 0000 0000 0000 0000 0000 1010 那么这个方法返回的值就是 28
根据 resizeStamp 的运算逻辑,我们来推演一下,假如 n=16,那么 resizeStamp(16)=32796
转化为二进制是
[0000 0000 0000 0000 1000 0000 0001 1100]
接着再来看,当第一个线程尝试进行扩容的时候,会执行下面这段代码
U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
rs 左移 16 位,相当于原本的二进制低位变成了高位
[1000 0000 0001 1100 0000 0000 0000 0000]
然后+2 = [1000 0000 0001 1100 0000 0000 0000 0000]+10
=[1000 0000 0001 1100 0000 0000 0000 0010]
高 16 位代表扩容的标记、低 16 位代表并行扩容的线程数
高 RESIZE_STAMP_BITS 位 | 低 RESIZE_STAMP_SHIFT 位 |
---|---|
扩容标记 | 并行扩容线程数 |
这样来存储有什么好处呢?
- 首先在 CHM 中是支持并发扩容的,也就是说如果当前的数组需要进行扩容操作,可以
由多个线程来共同负责 - 可以保证每次扩容都生成唯一的生成戳,每次新的扩容,都有一个不同的 n,这个生成
戳就是根据 n 来计算出来的一个数字,n 不同,这个数字也不同
第一个线程尝试扩容的时候,为什么是+2?
- 因为 1 表示初始化,2 表示一个线程在执行扩容,而且对 sizeCtl 的操作都是基于位运算的, 所以不会关心它本身的数值是多少,只关心它在二进制上的数值,而 sc + 1 会在低 16 位上加 1。
transfer()方法它把 Node 数组当作多个线程之间共享的任务队列,然后通过维护一个指针来划 分每个线程锁负责的区间,每个线程通过区间逆向遍历来实现扩容,一个已经迁移完的 bucket 会被替换为一个 ForwardingNode 节点,标记当前 bucket 已经被其他线程迁移完了。
- fwd:这个类是个标识类,用于指向新表用的,其他线程遇到这个类会主动跳过这个类,因为这个类要么就是扩容迁移正在进行,要么就是已经完成扩容迁移,也就是这个类要保证线程安全,再进行操作。
- advance:这个变量是用于提示代码是否进行推进处理,也就是当前桶处理完,处理下一个 桶的标识
- finishing:这个变量用于提示扩容是否结束用的
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//将 (n>>>3 相当于 n/8) 然后除以 CPU 核心数。如果得到的结果小于 16,那么就使用 16
// 这里的目的是让每个 CPU 处理的桶一样多,避免出现转移任务不均匀的现象,如果桶较少的话,默认一个 CPU(一个线程)处理 16 个桶,也就是长度为 16 的时候,扩容的时候只会有一 个线程来扩容
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
//构建一个nextTable对象,其容量为原来容量的两倍
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;
//创建一个 fwd 节点,表示一个正在被迁移的 Node,并且它的 hash 值为-1(MOVED),也就是前面我们在讲 putval 方法的时候,会有一个判断 MOVED 的逻辑。
//它的作用是用来占位,表示原数组中位置 i 处的节点完成迁移以后,就会在 i 位置设置一个 fwd 来告诉其他线程这个位置已经处理过了
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 首次推进为 true,如果等于 true,说明需要再次推进一个下标(i--),反之,如果是 false,那么就不能推进下标,需要将当前的下标处理完毕才能继续推进
boolean advance = true;
//判断是否已经扩容完成,完成就 return,退出循环
boolean finishing = false;
//通过 for 自循环处理每个槽位中的链表元素,默认 advace 为真,通过 CAS 设置 transferIndex 属性值,并初始化 i 和 bound 值,i 指当前处理的槽位序号,bound 指需要处理 的槽位边界;
for (int i = 0, bound = 0;;) {
// 这个循环使用 CAS 不断尝试为当前线程分配任务,直到分配成功或任务队列已经被全部分配完毕
// 如果当前线程已经被分配过 bucket 区域,那么会通过--i 指向下一个待处理 bucket 然后退出该循环
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
//--i 表示下一个待处理的 bucket,如果它>=bound,表示当前线程已经分配过 bucket 区域
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {//表示所有bucket已经被分配完毕
i = -1;
advance = false;
}
//通过 cas 来修改 TRANSFERINDEX,为当前线程分配任务,处理的节点区间为 (nextBound,nextIndex)->(0,15)
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//i<0 说明已经遍历完旧的数组,也就是当前线程已经处理完所有负责的 bucket
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 已经完成所有节点复制了
if (finishing) {
nextTable = null;
table = nextTab; // table 指向nextTable
sizeCtl = (n << 1) - (n >>> 1); //扩容阈值设置现在容量的0.75倍
return;
}
// 每增加一个线程参与迁移就会将 sizeCtl 加 1,这里使用 CAS 操作对 sizeCtl 的低 16 位进行减 1,代表做完了属于自己的任务
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//如果他们相等了,说明没有线程在帮助他们扩容了。扩容结束了。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
// 遍历的节点为null,则放入到ForwardingNode 指针节点
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//表示该位置已经完成了迁移,也就是如果线程 A 已经处理过这个节点,那么线程 B 处理这个节点时,hash 值一定为 MOVED
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 节点加锁,避免多线程复制同一个节点
synchronized (f) {
// 节点复制工作
if (tabAt(tab, i) == f) {
//ln 表示低位, hn 表示高位;接下来这段代码的作用 是把链表拆分成两部分,0 在低位,1 在高位
Node<K,V> ln, hn;
// fh >= 0 ,表示为链表节点
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) {//如果最后更新的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);
}
// 在nextTable i 位置处插上链表
setTabAt(nextTab, i, ln);
// 在nextTable i + n 位置处插上链表
setTabAt(nextTab, i + n, hn);
// 在table i 位置处插上ForwardingNode 表示该节点已经处理过了
setTabAt(tab, i, fwd);
// advance = true 可以执行--i动作,遍历节点
advance = true;
}
// 如果是TreeBin,则按照红黑树进行处理,处理逻辑与上面一致
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;
}
}
// 扩容后树节点个数若<=6,将树转链表
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;
}
}
}
}
}
}
上面的源码有点儿长,稍微复杂了一些,在这里我们抛弃它多线程环境,我们从单线程角度来看:
- 为每个内核分任务,并保证其不小于16
- 检查nextTable是否为null,如果是则初始化nextTable,使其容量为table的两倍
- 循环变量直到 finished,利用 tabAt 方法获得 i 位置的元素(支持多线程复制)
- 如果这个位置为空,就在原table中的 i 位置放入 ForwardingNode 节点,这个也是触发并发扩容的关键点;
- 如果这个位置的 hash 值为 MOVED,表示该位置已经完成了迁移;
- 如果这个位置是Node节点(fh>=0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在 nextTable 的 i 和 i+n 的位置上。并将 ForwardingNode 插入原节点位置,代表已经处理过了
- 如果这个位置是 TreeBin 节点(fh<0),也做一个反序处理,并且判断是否需要 unTreeify() 操作,把处理的结果分别放在 nextTable 的 i 和 i+n 的位置上。并插入ForwardingNode 节点
- 遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新 sizeCtl 为新容量的0.75倍 ,完成扩容。
在多线程环境下,ConcurrentHashMap用两点来保证正确性:ForwardingNode和synchronized。
- 当一个线程遍历到的节点如果是ForwardingNode,则继续往后遍历。
- 如果不是则将该节点加锁,防止其他线程进入,完成后设置ForwardingNode节点。
当其他线程处理该节点时可以看到已经处理过了,如此交叉进行,高效而又安全。
helpTransfer
在添加、删除等方法里面都会调用,当前优先协助扩容。
helpTransfer()方法为协助扩容方法,当调用该方法的时候,nextTable一定已经创建了,所以该方法主要则是进行复制工作。
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 判断此时是否仍然在执行扩容,nextTab=null 的时候说明扩容已经结束了
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) {//说明扩容还未完成的情况下不断循环来尝试将当前 线程加入到扩容操作中
/**
* 下面部分的整个代码表示扩容结束,直接退出循环
* sc >>> RESIZE_STAMP_SHIFT !=rs, 如果在同一轮扩容中,那么 sc 无符号右移比较高位和 rs 的值,那么应该是相等的。如果不相等,说明扩容结束了
* sc==rs+1 表示扩容结束
* sc=rs+MAX_RESIZERS 表示扩容线程数达到最大扩容线程数
* transferIndex<=0 表示所有的 Node 都已经分配了线程
*/
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
//在低16位 上增加扩容线程数
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);//帮助扩容
break;
}
}
return nextTab;
}
//返回新的数组
return table;
}
转换红黑树
用于将过长的链表转换为TreeBin对象。但是他并不是直接转换,而是进行一次容量判断。
-
如果容量没有达到转换的要求(table.length<64),直接进行扩容操作并返回;
-
如果满足条件才链表的结构抓换为TreeBin ,这与HashMap不同的是:
1.根据table中index位置Node链表,重新生成一个hd为头结点的TreeNode
2.根据hd头结点,生成TreeBin树结构,并用TreeBin替换掉原来的Node对象。
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)//tab 的长度是不是小于 64, 如果是则优先执行扩容
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;
//构造了一个TreeBin对象 把所有Node节点包装成TreeNode放进去
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);//这里只是利用了TreeNode封装 而没有利用TreeNode的next域和parent域
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
//在原来index的位置 用TreeBin替换掉原来的Node对象
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
get函数
读取操作,不需要同步控制,比较简单
- 空tab,直接返回null
- 计算hash值,找到相应的bucket位置,为node节点直接返回,否则返回null
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)
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;
}
参考链接:
https://blog.csdn.net/programmer_at/article/details/79715177#141-addcount
https://blog.csdn.net/u010723709/article/details/48007881
https://www.jianshu.com/p/c0642afe03e0
http://cmsblogs.com/?p=2283