目录
ConcurrentHashMap和HashMap和Hashtable三者的区别
前言
小伙伴们在面试的时候,可能会被面试官问到你知道线程安全的map集合有哪些吗?有了解过ConcurrentHashMap集合吗?ConcurrentHashMap集合为什么线程安全?保证线程安全为什么不使用Hashtable等一系列的连环考问,所以给大家带来ConcurrentHashMap集合的底层源码解读。
注意:只讲解jdk1.8的HashMap和ConcurrentHashMap
复习HashMap
在jdk1.8的ConcurrentHashMap也是变成跟HashMap一样的数据结构,所以开始之前先复习一下jdk1.8的HashMap。
- HashMap没有任何锁机制,所以线程不安全
- HashMap底层维护了Node数组+Node链表+红黑树。
- HashMap初始化和扩容只能是2的乘方
- HashMap负载因子阈值是数组的0.75
- HashMap链表尾插法
- HashMap是懒加载机制
- HashMap单链表大于8,数组长度大于64变成红黑树提高链表的查找速度。
- HashMap无序(根据hash值确定数组位置)不重复(重复就是替换)。
- HashMap的key和value允许为null
- .......
而且jdk1.7到jdk1.8的hash算法也进行了升级,尽量避免hash冲撞。
ConcurrentHashMap和HashMap和Hashtable三者的区别
HashMap:线程不安全
Hashtable:线程安全但是效率低
ConcurrentHashMap:线程安全,相比Hashtable效率高
HashMap在多线程下是线程不安全的map集合,作为老一辈的Hashtable通过synchronized同步锁来保证线程安全,但是为什么如今淘汰被ConcurrentHashMap代替呢?就是因为锁的力度太大了,只要是Hashtable中的方法都加上了synchronized。ConcurrentHashMap同样在jdk1.8中加入了synchronized为什么就效率如此高呢?我们下面开始追寻源码!
hello world代码准备
public class ConcurrentHashMapTest {
public static void main(String[] args) {
Teacher t1 = new Teacher("haha1",15);
Teacher t2 = new Teacher("haha2",15);
Teacher t3 = new Teacher("haha3",15);
Teacher t4 = new Teacher("haha4",15);
Teacher t5 = new Teacher("haha5",15);
Teacher t6 = new Teacher("haha6",15);
Teacher t7 = new Teacher("haha7",15);
Teacher t8 = new Teacher("haha8",15);
Teacher t9 = new Teacher("haha9",15);
Teacher t10= new Teacher("haha10",15);
Teacher t11 = new Teacher("haha11",15);
Teacher t12 = new Teacher("haha12",15);
Teacher t13 = new Teacher("haha13",15);
Teacher t14 = new Teacher("haha14",15);
ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap();
concurrentHashMap.put(t1.getName(),t1);
concurrentHashMap.put(t2.getName(),t2);
concurrentHashMap.put(t3.getName(),t3);
concurrentHashMap.put(t4.getName(),t4);
concurrentHashMap.put(t5.getName(),t5);
concurrentHashMap.put(t6.getName(),t6);
concurrentHashMap.put(t7.getName(),t7);
concurrentHashMap.put(t8.getName(),t8);
concurrentHashMap.put(t9.getName(),t9);
concurrentHashMap.put(t10.getName(),t10);
concurrentHashMap.put(t11.getName(),t11);
concurrentHashMap.put(t12.getName(),t12);
concurrentHashMap.put(t13.getName(),t13);
concurrentHashMap.put(t14.getName(),t14);
concurrentHashMap.entrySet().stream().forEach(item -> System.out.println(item));
}
}
class Teacher{
private String name;
private int age;
public Teacher(){
}
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Teacher{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Teacher teacher = (Teacher) o;
return age == teacher.age && Objects.equals(name, teacher.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
正文
ConcurrentHashMap源码解读
开始之前先了解一下底层的数据结构
在jdk1.8ConcurrentHashMap的底层结构迎来了一个大改变,跟HashMap的底层接口一样。
ConcurrentHashMap底层是一个懒加载,我们可以看到构造方法,就算是带参构造方法也是将值变成2的乘方然后保存起来并没有初始化。
因为是懒加载,所以就是我们添加值的时候我们初始化,就给put()方法来上一个断点。
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 这里跟HashMap不一样,这里不运行key和value存在null值。
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;
// 判断是否是null或者长度为0,也就是是否需要初始化
if (tab == null || (n = tab.length) == 0)
// 初始化的方法,默认长度是16,负载因子的阈值是16*0.75=12,负载因子决定扩容时机
// 这里初始化的时候,因为会存在并发初始化的情况,所以内部使用unsafe的cas操作保证
// 初始化的原子性
tab = initTable();
// 这里使用hash值在通过算法获取到插入位置,再获取到插入位置是否有值。没值就直接添加。
// 因为要保证线程安全,所以ConcurrentHashMap内部维护了一个Unsafe使用cas操作保证
// 原子性,所以多线程的情况下是安全的
// tabAt()方法取值也是通过unsafe的取值。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// ForwardingNode节点MOVED为-1,达标已经有线程在移动扩容了,此时我们帮助移动
// 帮助移动完毕就产生新的Node数组,这里就再进入For循环找位置插入
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 能进到else中,就代表当前进来的值已经是hash冲撞了
else {
// 临时变量
V oldVal = null;
// 同步锁,但是这个同步锁锁的对象是冲撞的head节点,所以他的锁的力度不大
// 对于其他数组节点、数组链表、数组红黑树的添加操作不影响。
synchronized (f) {
// double check操作,因为之前获取的可能数组扩容变换位置了。
if (tabAt(tab, i) == f) {
// fh是啥? 没错他就是hash值,从上一个else if判断中获取到的
// hash值为正数就代表是非红黑树的添加
if (fh >= 0) {
// 计数器+1,计数器为了后面的扩容做计数
binCount = 1;
// 因为进到这里肯定是hash冲撞了,所以需要判断是否是同一个key
// 因为map集合是无序不重复,所以相同的key就是替换value值。
// 但是也有可能只是hash值冲撞了,但是key值并不相同
// 所以就产生了单Node链表,所以这里循环也是在遍历链表
for (Node<K,V> e = f;; ++binCount) {
// 存储已经存在链表的的key值,并非本次添加的key值。
K ek;
// 判断是否相等,相等就替换
// 其实从这里就可以得出hashcode相等,equals不相等
// 但是equals相等他的hashcode必然相等
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 如果是相等了就把旧值赋值给临时变量,并且作为返回值返回
oldVal = e.val;
// 对于concurrentHashMap来说默认是不重复,
// 但是可以通过onlyIfAbsent变量来控制新来的值是否替换
// 是使用之前的还是新的
if (!onlyIfAbsent)
e.val = value;
break;
}
// 走到这里就达标hashcode冲撞了,但是key值并不相等
// 所以这里就判断链表的next节点是否为null,为null就添加
// 不为null就进入到下一次循环,下一次循环就继续走上面的代码
// 继续判断是否key值相等,不相等又来到这里,直到next节点为null
// 大家都知道链表长度为8转红黑树,所以这里又可以得出是大于等于8
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 红黑树的添加node节点
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;
}
}
}
}
// 判断是否从链表转成红黑树结构
// 条件是单链表大于等于8切数组长度为64,如果数组长度不满足64就扩容
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
// 判断是否key值重复,重复了就返回旧的value值
if (oldVal != null)
return oldVal;
break;
}
}
}
// 这里是添加一次计数器的值,并且达到负载因子的阈值就会进行扩容
addCount(1L, binCount);
// 如果是没有key值重复的情况就是返回null,如果key值重复就是返回旧的value值
return null;
}
逐行的代码介绍。
核心代码都在这里了,大家分析之前可以用记事本之类的工具来记录变量因为“Doug Lea”程序员写的代码虽然简洁优美但是读起来有点费力。
我们来分析为什么线程安全?
首先,Node数组初始化的时候,如果没有控制并发,那么可能就会存在多次初始化的操作。那么我们来看看ConcurrentHashMap如何控制的。给 initTable()方法来上一个断点并且追进去。
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
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;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
yield():会让出当前线程的cpu资源,造成一个上线文的切换,并且进入到EntryList中排队继续尝试等待cpu调度。
sizeCtrl默认值是0因为是int类型,所以进入到else if代码块中进入到cas自旋来改变成-1因为-1的定义是在初始化,并且其他没获取到的就返回false进入到下次while循环了,然后再if就进入到Thread.yield()。
我们看到通过cas自旋抢到资格的线程给Node数组初始化的过程,其实也就是给上默认16的大小,负载因子阈值就是n - (n >>> 2);也就是数组长度的0.75倍。但是还记得ConcurrentHashMap的有参构造方法吗?他定义Node数组的初始大小,所以第一次在sc = sizeCtl进行了赋值,后面的int n = (sc > 0) ? sc : DEFAULT_CAPACITY;三目运算符。
我们继续回到putVal()方法中。
从整体代码中就看到了一个synchronized代码块,那么synchronized之前的操作就不需要上锁了吗?
我们再思考,synchronized 前都是一些什么操作,刚刚已经说过初始化它是使用cas自旋来保证原子性,那么还有hash值没有碰撞直接添加到Node数组中的操作他就不需要上锁吗?
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
所以我们追到casTabAt()方法中。
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);
}
没错又是cas来保证原子性,所以说,多个线程来同时竞争到Node数组的同一插槽时通过cas来竞争,没抢到的就返回false,就进入到下次循环,下次进来通过hash算法还是同一个插槽所以就进入到else中的循环来看是否是key值重复或者是hash冲撞添加到链表尾部。
单线程扩容
再思考,我们扩容添加到数组节点或者链表节点或者红黑树节点中呢?那么多线程情况下扩容和重新添加新数组如何保证原子性的呢?
我们hello world代码特意put了十几个对象,我们在直接断点调到第12个添加,并且我们直接进入addCount()方法中,进入到扩容transfer(tab, null)方法;
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
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
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
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);
return;
}
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
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
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) {
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);
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;
}
}
}
}
}
}
我只能说不难,但是特别的复杂,因为变量太多了。
这里我们就不一行一行的看了,我直接告诉你们理论,大家通过理论自己debug追吧。
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
这两行代码证明了扩容是原有的一倍,并且ConcurrentHashMap内部维护了一个扩容时的临时的Node数组,用来把原来Node数组的值迁移过去。如图所示
从原来数组最后一位开始,如果为null,就直接给上ForwardingNode类型的标志节点。如图所示。
如果有值会判断是否是head节点,也就是是否存在链表,存在链表就取到链表最后一位。如果不存在链表就直接通过setTabAt()方法将原有的Node节点转移到新数组中,并且将原数组的Node节点给设置上ForwardingNode类型的标志节点。
注:红黑树部分博主没有去追,想追的同学可以看到transfer方法中红黑树部分的迁移
大致就是这样的循环来遍历,当遍历完第一个节点以后,回回到最后一个节点再重新循环判断hash码是否是-1,因为ForwardingNode节点的hash值都为-1。检查完以后将nextTable赋值给table,然后将nextTable置为null。这样整个扩容就结束了。
多线程扩容
虽然扩容是结束了,但是我们的初衷不是为了追寻多线程情况下的扩容吗?而这里只是一个单线程的扩容。所以下面是多线程扩容。
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
首先看到putVal()方法的else if代码块中,判断hash值是否为-1,在前面的单线程中我们可以知道,new出来的ForwardingNode节点的hash码就为-1,所以就达标当前putVal的key插入的位置已经在扩容的迁移过程了,所以当前插入的线程就执行helpTransfer()方法,从方法名就能知道帮助迁移,所以我们又可以得出一个结论就是在多线程情况下是多线程扩容。
我们再看到addCount()方法和helpTransfer()方法
// helpTransfer方法
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);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
// addCount()方法
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
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;
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();
}
}
}
大致解读一下,他们有相同的代码就是通过cas操作来改变sizeCtl的值,还记得在Node数组初始化的时候,sizeCtl的用途是记录负载因子的阈值,而在这里的用途就是记录一共有多少个线程一起并发扩容。在addCount()方法中第一个if代码块是记录并发累加或减去计数器的值,当计数器的值达到了阈值就要产生扩容,而增删改都会影响到计数器的值。
我们回到transfer()方法中。
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
NCPU是当前电脑的cpu核数,所以说如果对于单核cpu来说就是Node数组全部节点的扩容都交给一个线程,因为单核也不存在并发,那么对于多核来说呢就是当前数组长度右移3位,也就是除以8再除以cpu核数如果小于默认的16就当前线程处理的16个节点。
我们再往后走,下面的if (nextTab == null) 判断如果是多线程的情况下并没有加锁肯定是能有多个线程进去的,但是代码块中的内容是扩容的初始化操作,那么能运行多线程访问这块,肯定后面有cas锁来控制。我们接着往后走
我们看到while循环中的代码块
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
这里并没有上锁,所以我们要考虑多线程进行这些代码。
在前面介绍的if (nextTab == null) 初始化代码中对transferIndex = n;对transferIndex做了一个赋值,n是数组的长度,所以while循环第一次只能进入到最后的else if中。我们看到判断条件
很显然是cas自旋,所以就能明白这里在控制并发了。而且还是对transferIndex变量做的处理,
nextIndex在上一个else if中赋值,就是transferIndex变量值,而transferIndex变量值又是Node数组的长度,而stride默认是16,所以三目运算符走:左边的等式,数组长度减去16,所以说没抢到cas的线程下次cas自旋就是获取的是总数组长度-16位开始处理。并且当nextIndex=16的情况下就是为0。所以从这里就能明白是可以多线程协作迁移。然后通过将nextBound长度赋值给临时变量bound和临时变量i来控制当前抢到cas的线程处理迁移的下标。
我们再往下走。
最后面那个else代码块,就是最长的那个else,我们注意到加了synchronized同步代码块,这个跟putVal()方法的synchronized一样,首先锁的是一个链表或者是红黑树,并没有锁整个Node数组,所以来说效率是非常高的,不会影响到其他线程干活。甚至你在扩容的时候,你还能往其他位置添加Node节点。
我们再由上图继续做思考假如我们只有2个线程,那么最前面4个节点谁来处理呢?
回到我们的while循环
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
当某个线程已经迁移完改迁移的节点后,第一个if通不过,又来到else if中,获取到当前的transferIndex,肯定是大于0的所以继续往下一个else if走,所以这里又分配了任务,通过cas来控制只有一个线程得到了这个迁移任务!
再思考多线程的迁移,怎么判断彻底迁移完成呢?
再我们进入transfer()方法前,我们通过sizeCtl变量来记录一共有多少个线程来并发执行。所以这里肯定是通过sizeCtl变量来决定是否全部线程完活。
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 减去迁移前添加的-2
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
}
}
// 扩容迁移前给sizeCtl加2
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
单线程的时候说了,某个线程迁移扩容后,会检查原本数组节点是否都已经是ForwardingNode
并且当全部线程检查完以后然后做一个新旧数组的赋值和sizeCtl 的初始化然后就完成了扩容。
总结
对于“Doug Lea”先生的代码,我觉得真的佩服到五体投地,甚至觉得不是人写出来的代码真的太牛了,但是可读性确实很差这点不可否认,反正博主追的时候都是那个记事本来记录变量。
如此的复杂的代码我认为用一篇帖子来讲明白肯定不现实。但是博主真的在用心写想通过画图的方式来让读者读懂,并且我认为这种源码,大家可以先通过帖子和视频知道每步大概在干嘛先知道一个结果,然后debug来追、
课程推荐(免费)
为了读不懂的小伙伴,我特意找到了好理解的视频课程,免费的绝对良心推荐
并非只有ConcurrentHashMap,并发包内容都有讲,特别质量,容易理解https://www.bilibili.com/video/BV16J411h7Rd?p=277特别的仔细,应付面试没问题,不过建议自己一定要debug哦https://www.bilibili.com/video/BV17i4y1x71z?from=search&seid=15580606268053490496&spm_id_from=333.337.0.0
最后,写帖不易,希望大家点赞收藏+关注,一直有在分析质量帖和质量视频。