开…开…开始了,久负盛名的ConcurrentHashMap。
之前一直是看别人文章解析它,今天终于轮到我了。就像以前读书的时候老师说的,写下来理解了,才是自己的。事先声明,这篇文章虽然字数有点多,那是因为有很多代码,然后有些比较绕的地方我做了详细的说明,因此字数确实有点多,但是如果耐心看下去,相信你一定有收获的。
ConcurrentHashMap(以下简称chm)是concurrent包下的用于多线程并发的一个类,由Doug Lea大神编写。它比HashMap更复杂(其实HashMap也没有很复杂 ),由于它是线程安全的同时效率还比HashTable高,因此CHM一直备受瞩目。不多废话,直接上源码了。
首先,1.8的CHM和1.7是有很大的区别的,1.8的CHM结构上和HashMap1.8一样,也是数组+链表/红黑树,但是由于支持并发访问,因此源码是比HashMap复杂许多的。1.7和1.8的CHM还有其他区别现在就先不说了,以后会写的。
再有,给个前提。CHM1.8是采用了 CAS + synchronized 来保证并发安全性(关于CAS和synchronized以后会也写相关的文章的)。带着这个前提去看可能更能理解一点。
注:虽然是源码解析,但是并不是所有的源码都会涉及到,只涉及到经常使用的那些。同时,也不是所有的都会解释的很清楚,还得自己亲自去看,那才会是自己的。
首先看看CHM的类关系图,了解一下它的继承关系和实现的接口。
常用常量和变量
先来看一些常量。
// chm能接受的最大的容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的初始化容量
private static final int DEFAULT_CAPACITY = 16;
// 能转成array最大的阈值。
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;
// 如果没达到这个容量,会先扩容,而不是树形化。这样避免调整大小和树形化冲突。
// 这个值一般最少是4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
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中记录size大小的偏移量,为16
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// node的hash值。
static final int MOVED = -1; // forwarding nodes的hash值
static final int TREEBIN = -2; // 树的根节点hash值
static final int RESERVED = -3; // 暂时保留的hash值
常用变量如下:
// CHM的数组,通常到第一次插入时才被初始化,大小总是为2的次方
transient volatile Node<K,V>[] table;
// 扩容时的新数组。
private transient volatile Node<K,V>[] nextTable;
// 基础计数器。
private transient volatile long baseCount;
// 控制数组初始化以及扩容阈值的变量。
private transient volatile int sizeCtl;
有一些HashMap中有的就不说了,如负载因子,树形化阈值等。
这里只讲解一个变量,sizeCtl,它是用于控制数组初始化以及扩容的变量,它的值不同代表着不同的意思:
- 为-1时,表示正在初始化中。
- 为-n时,表示有(n - 1)个线程帮助扩容。
- 为0或是正值时,表示还没有初始化,那么这个值就表示初始化或是下次扩容的大小。
可以看见sizeCtl为负数时,不是在初始化就是在扩容。
最后,需要引起注意的是,所有的变量都使用volatile修饰了!!volatile是保证了可见性的。
构造函数
无参的就不放上来了,就看几个有参的构造函数吧。
// 给了初始化容量。
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
// 给cap赋值为大于等于(1.5倍initialCapacity + 1)的2次方的数
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
// 将cap赋值给sizeCtl
this.sizeCtl = cap;
}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
// 给了初始化容量,负载因子,并发数的。
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
构造函数做个了解就行,一般都是使用无参的构造函数。有一个地方需要引起注意,就是使用tableSizeFor方法时,传递的是
1.5 * initialCapacity + 1,也就是说,如果你的initialCapacity是10,那么真正的大小是16,如果传的是11,那么真正大小是32。这里和HashMap1.8是不一样的。
常用api
put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
// onlyIfAbsent为true时,不会改变已经存在的值,也就是说,只有key不存在时才会put。
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 (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 没有初始化数组就先初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 判断该key落在数组的哪个位置,然后将第一个节点赋值给f。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果该节点为null
// 通过cas将该节点放置到该位置
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果f的hash值为MOVED(-1),那么帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else { // 如果不是,那就将插入
V oldVal = null;
synchronized (f) { // 使用sychronized锁住头节点。
// 再判断头节点是否是f。
if (tabAt(tab, i) == f) {
// 如果头节点的hash值大于0,说明是链表
if (fh >= 0) {
// 记录链表长度的,因为进来了说明头节点肯定不为null,因此赋值为1
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果hash值相等,同时key值一样,或是key值相等。
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;
// 使用红黑树的方法插入进红黑树
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)
// 这里和HashMap1.8是一样的,如果数组大小小于64,那么会先进行扩容而不是树形化
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 将大小(即baseCount)+1
addCount(1L, binCount);
return null;
}
一路看下来,put方法其实也不难,梳理一下流程。
- 首先拿到key的hash值,然后看数组是否初始化,没有的话,先初始化。
- 判断hash值应该在数组中的哪个位置,如果该位置上没有任何节点,使用cas插入即可。
- 如果已经有节点,判断该节点的hash值是否等于MOVED,该值表示正在扩容,因此,如果是,那么帮助扩容。如果不等于,那么使用sychronized获取头节点的监视器锁。避免多个线程并发插入。
- 判断数组中是链表还是红黑树, 然后遍历,如果已经有该节点,那么判断能否覆盖,再决定是否覆盖。
- 如果没有,则使用尾插法(如果是链表)插入到末尾。
- 最后,判断是否需要树形化。当然,在树形化之前会先判断数组大小是否小于64,如果小于,会先扩容而不是树形化。
这里有几个方法还没写到,初始化数组,扩容,以及数据迁移。不过后面都会提到的。
initTable 方法
该方法的作用是初始化数组的。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 当数组为空,或是长度为0时
while ((tab = table) == null || tab.length == 0) {
// 如果sizeCtl 的值为负值,说明正在初始化,那么线程让步。
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 如果不是为负值,那么通过cas将sizeCtl值设为-1
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 再次判断是否未初始化或是长度为0
if ((tab = table) == null || tab.length == 0) {
// 如果sizeCtl大于0,那么设置为传来的值,否则为默认值,16.
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 新建一个数组,长度为16或是传来的值
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 将新建的数组传给变量table,该table是volatile的。
table = tab = nt;
// 将sc的值设为n的0.75倍。
// 如果n一开始默认是16的话,那么这里sc就是12.
sc = n - (n >>> 2);
}
} finally {
// 将sc的值赋给sizeCtl
sizeCtl = sc;
}
break;
}
}
return tab;
}
初始化方法比较简单,简而言之,就是先判断数组是否已经初始化,同时sizeCtl是否是负数了,如果已经是负数了说明已经有线程在初始化,那么直接让步就行。如果不是,说明还没有线程初始化,这个时候正在执行的线程就可以通过cas将sizeCtl设为-1,表示自己已经在初始化了,cas成功之后,还需要判断一次是否已经初始化,因为很有可能轮到当前线程cas成功进入之后,其实已经有线程初始化完成了。
下面看看重头戏,扩容。
扩容方法
addCount方法
更新baseCount大小,同时判断是否需要扩容。
// 将baseCount大小+x
// x是要加的大小
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 如果通过cas+1失败,执行下面的方法。这里不细讲了。
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
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;
// 如果大小+x之后大于扩容阈值,那么就执行是否扩容操作。
// 由于这个方法和tryPresize方法类似,直接看下面的。
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
addCount方法过一下就行,在判断sc < 0下的逻辑在tryPresize方法后面会详细讲解。
tryPresize方法
private final void tryPresize(int size) {
// c的值为大于等于size的1.5倍+1的2次方。表示要扩容的大小。
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
// 当sizeCtl的值大于等于0,表示没有线程在扩容。
while ((sc = sizeCtl) >= 0) {
// tab赋值为table。
Node<K,V>[] tab = table; int n;
// 这块和初始化数组基本上来说是一样的,就不再阐述了。
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
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_CACTY)
break;
// 如果tab还是等于table的话。
else if (tab == table) {
// n是当前数组长度,该方法返回n的生成戳
// resizeStamp会返回格式为0000 0000 0000 0000 1xxx xxx xxxx xxxx的数。
// 请注意,这个数的第17位必定是1,也就是说左移16位,必定是个负数。
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;
// 否则参与进去。
// 其实如果看后面,也就是transfer方法中,每参与一个,sizeCtl值就会+1,表示多一个线程参与进来。
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 如果sc大于等于0,表示目前还没有扩容的线程。
// 通过cas将sizeCtl的值设为(rs << RESIZE_STAMP_SHIFT) + 2
// 上面我们知道rs的第17位是1,因此,左移16位,必定是个负数,还是个蛮大的负数。
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
这里的代码确实比较复杂,CHM1.8难就难在理解扩容这块。但是慢慢去体会,去思考,还是可以理解的,毕竟代码也是人写的。由于已经在注释中写的很清楚了,这里就不再赘述了。不过还是有几个点可能比较绕,有点让人无法理解,因此在这里详细说明一下。
- resizeStamp(n) 方法有两个作用:
- 计算出n的前导0的个数假设作为m,由于每个n的前导0个数肯定是不一样的,因此,m就是一个n的印记。
- 使m的第17位(从1开始计算的)为1,然后返回给 rs,因此,rs就形如是0000 0000 0000 0000 1xxx xxx xxxx xxxx的数。
- 可以看见,在进入transfer(tab, null)方法之前,会将sizeCtl设为(rs << RESIZE_STAMP_SHIFT) + 2, 由于rs 的第17位为1,因此左移16位,会使得sizeCtl 必定是一个负数,是负数就代表必定有线程在扩容了,同时由于左移16位了,sizeCtl 的高16位表示了这个线程是在扩容数组大小为 n 的!!
- 有了上面的前提,那么,在进入了判断条件为 sc < 0 的一连串判断就比较好理解了。
- (sc >>> RESIZE_STAMP_SHIFT) != rs, sc 无符号右移如果不等于 rs, 即 sc 的高16 位与 rs 不等,因此说明,印记不一样,就说明不是同一个容量的扩容操作,因此,不参与扩容。
- sc== rs + 1和 sc== rs + MAX_RESIZERS ,就是判断帮助迁移的线程数是不是已经达到了运行帮助迁移的最大线程数。是,该线程肯定就不再帮助了。
- (nt = nextTable) == null和transferIndex <= 0说明已经迁移完成了,那肯定不用该线程再去帮忙。
- 最后一个问题可能是,为什么第一个数据迁移的线程要将sizeCtl设为(rs << RESIZE_STAMP_SHIFT) + 2,rs << RESIZE_STAMP_SHIFT的原因前面已经说过了,至于+2 ,是使sizeCtl低16位代表有多少个线程在进行数据迁移,由于sizeCtl == -1 表示的是正在初始化,所以 +2 ,同时也代表了有一个线程已经在数据迁移了。,前面也说过sizeCtl为-n时,表示有(n - 1)个线程帮助扩容。
不过这里我有一个疑惑,就是按理应该不可能进入if (sc < 0) 中的,因为while循环的条件就是(sc = sizeCtl) >= 0,而sc又不是由volatile修饰的,不存在可见性的可能,希望有知道的人告知一声,谢谢。
transfer方法
重头戏来了,数据迁移方法。可能直接看源码有点难,我先来讲解一下前提,带着这个前提去理解就会好很多。
数据迁移,就是类似HashMap一样将原数组的数据迁移到新数组中去,但是由于CHM要保证线程安全性的同时还要保证一定的效率,因此,做了很多的工作。
首先,假设原数组大小为n,那么就有n个要迁移的数据任务(不是说要迁移的节点只有N个,而是每个数组中的链表/红黑树算一个任务),然后让多个线程负责这些小任务,每个线程需要负责的小任务数量用stride表示,这样就能达到多个线程同时迁移数据的目的。
可想而知,如果要知道线程要处理从哪里开始的小任务,那肯定要有一个全局性的变量去控制,任务分配到了哪个位置,这样才能让A(或其他线程)知道应该从哪处理。transferIndex就是这个全局性的变量。
同时,还要知道,从下面的源码可以看出,transferIndex最开始是赋值为数组大小的,因此,说明是从后往前迁移的。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 如果单核情况下stride 直接为n,如果是多核情况下就是(n >>> 3) / NCPU
// 但是stride 最小为16,说明一个线程最少处理16个小任务。
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 新数组还未初始化
if (nextTab == null) { // initiating
try {
// 创建一个新数组,大小为当前数组大小的2倍。
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
// 将创建的新数组赋值给nextTable
nextTable = nextTab;
// transferIndex 指向原数组的最后一个位置,说明数据迁移是从后向前的。
transferIndex = n;
}
int nextn = nextTab.length;
// ForwardingNode,就是之前常量中提到的MOVED的那个!!!
// 这个ForwardingNode表示正在迁移的节点!!hash值为MOVED,也就是-1。
// 也就是在putVal如果头节点的hash值为MOVED就调用helpTransfer帮助数据迁移!!!
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 表示true说明已经做过了数据迁移,可以做其他的小任务了。
// 其实从advance翻译也看的出来,前进的意思。
boolean advance = true;
// 表示是否已数据迁移完成。
boolean finishing = false; // to ensure sweep before committing nextTab
/**
* 由于是从后向前迁移的,i表示迁移开始的地方,bound 表示迁移的边界
**/
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 前面也说了为true是说明已经做过迁移了,因此一直找下一个位置需要迁移的位置,直到找到为止。
// 想想看,刚进来的线程是不是什么任务都没做,因此,它可以接任务了。
while (advance) {
int nextIndex, nextBound;
//
if (--i >= bound || finishing)
advance = false;
// 将下个要迁移的位置设为nextIndex
// 如果 <= 0,说明数据已经迁移全部完了,不用迁移了。
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 否则,说明还是有数据要迁移的,那么判断nextIndex的位置是否是大于stride。
// 是,则transferIndex改为nextIndex - stride
// 为什么判断是否大于stride,因为前面也说了,每个线程处理的小任务数以stride 起步的。
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// 将bound 设为迁移任务的边界
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
// 到这里了表示已全部迁移完成,做一些相应的操作就可以直接return了。
nextTable = null;
table = nextTab;
// 将下次的扩容阈值设为新数组大小的0.75倍
sizeCtl = (n << 1) - (n >>> 1);
return;
}
/**
* 前情提示:
* 在tryPresize中sizeCtl的值设为(rs << RESIZE_STAMP_SHIFT) + 2,rs就是resizeStamp(n)
* 然后每有一个线程参与进来,sizeCtl就+1。
* 因此,到了这里,又将sizeCtl-1了。
**/
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 到了这里,说明(sc - 2) 肯定是等于resizeStamp(n) << RESIZE_STAMP_SHIFT的
// 也就是说,所有参与进来的线程都完成了自己的任务了。
// 然后就可以进上面的if (finishing)方法了!
finishing = advance = true;
i = n; // recheck before commit
}
}
// 否则,找到数组中的i位置,如果该位为null,那么将之前的ForwardingNode放过来。
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 否则,表示已迁移过了。
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else { // 到这里就是表示进入的该线程终于可以迁移了!!!
// 使用sychronized获取头节点的监视器锁。
synchronized (f) {
// 再次判断头节点是否相等,因为有可能又有线程抢在你前面迁移数据了。
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 如果是链表,走这里。
if (fh >= 0) {
/**
* 将当前节点和 n(数组大小) 相与,这里的原理和HashMap是一样的。
* 如果和 n 相与为0,说明在新数组中的位置没变,如果是1,则新位置为原位置+n
* 因此,在原位置上的是一条链表,在原位置+n的又是一条链表。
**/
// 拿到头节点和 n 的相与值
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;
}
}
// ln 代表的是在新数组原位置的,hn则是另一个的。
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);
// 将原数组该位置设置为fwd的。
// 看上面也知道,其他线程看到了就知道已经有线程处理过这个了。
setTabAt(tab, i, fwd);
advance = true;
}
// 红黑树的数据迁移,这里不细讲了,和上面差不多。
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
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;
}
}
}
}
}
}
其实要说的已经在前提和注释中说的很清楚了,但是还是简单的概括一下吧。
- 首先stride最少是16,也就是说每个线程处理的小任务数最少是16的。
- 如果nextTable为null,就说明进来的这个线程是参与数据迁移的第一个,因此它比其他的线程多了一个初始化任务。
- 新建一个ForwardingNode节点,真正迁移数据时,这个节点会放在数组的头节点处,这个节点的hash值为MOVED(即-1),是一个标志性的存在,告知其它进来的线程,这个位置已经处理过了。同时,回看putVal的代码,如果获取的头节点的hash值是MOVED的,那么就会调用helpTransfer方法来参与进扩容这一步!
- 进入while循环直到找到还没有进行迁移的位置,让人如果已经没有还需要迁移的,肯定就跳出来。
- 跳出while循环后,说明要么任务已经全分配出去了,要么就找到分配给这个线程任务的位置了,那么首先判断是不是已经全部分配且迁移完了,如果是就用新数组替换老数组同时将sizeCtl阈值设为新数组大小的0.75倍,也就是阈值。
- 如果不是,那么判断要迁移的位置上的头节点是否为空或者已经设为MOVED,代表已迁移了。那就继续走while循环找新的任务,直到找到可以迁移的位置。
- 找到后,就可以真正进行迁移了,会根据该位置上头节点的hash值判断是红黑树还是链表再放到不同的位置上。
呼,总算是讲完了这里的逻辑,能看到这里并理解了的你真棒!
在这里我就不放helpTransfer方法了,只要能看懂这个,helpTransfer肯定也不在话下。
get方法
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;
}
// 如果头节点的hash值为负数,说明,要么在扩容,要么是红黑树。
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;
}
由于比较好理解,就没写太多注释。
最后
鉴于文章篇幅的原因,就不写总结了,该写的,都已经写的很详细了。其实CHM1.8的代码也不算复杂,但是其设计之精巧,真的让人佩服不已,Doug Lea大神是真的厉害。
以上,是关于ConcurrentHashMap1.8的全部内容。
谢谢各位的观看。本人才疏学浅,如有错误之处,欢迎指正,共同进步。