Java 8 ConcurrentHashMap中sizeCtl低16位为何从1而非0统计扩容线程数?
首先纠正一个常见误解:低16位从1开始计数不是为了给初始化状态-1预留空间。因为初始化状态的-1是全1的二进制值,而扩容状态的sizeCtl高16位是由resizeStamp(n)生成的固定标记(不会是全1),所以无论低16位是什么值,扩容状态的sizeCtl都不可能等于-1。
真正的设计原因和扩容流程的逻辑一致性有关:
扩容线程的计数逻辑:
启动扩容时,sizeCtl被设置为(rs << RESIZE_STAMP_SHIFT) + 2,这里的+2对应1个活跃扩容线程(线程数 = 低16位值 - 1)。后续每有一个新线程加入扩容,就会通过CAS把sizeCtl加1(线程数同步+1);每有一个线程完成扩容任务,就会把sizeCtl减1(线程数同步-1)。扩容完成的判断依据:
当sizeCtl减到(rs << RESIZE_STAMP_SHIFT) + 1时,低16位值为1,对应线程数为0,说明已经没有活跃的扩容线程了。此时最后一个完成任务的线程会触发扩容收尾工作:将sizeCtl设置为新表的容量阈值(新容量 * 0.75)。
如果低16位从0开始计数,初始启动时sizeCtl会是(rs<<16)+1,线程完成后会减到(rs<<16)+0,虽然也能判断扩容结束,但这样的设计会让“扩容中”(线程数>0)和“扩容结束”(线程数=0)的状态区分不够直观,且需要调整后续的判断逻辑。源码逻辑的自洽性:
源码中的判断条件sc == rs + 1(实际等价于sc == (rs << RESIZE_STAMP_SHIFT) + 1,rs是resizeStamp(n)的返回值,高16位为0,左移16位后才是扩容标记的高16位),就是用来判断当前是否已无活跃扩容线程,从而终止当前线程的扩容尝试。如果低16位从0开始计数,这个条件就要改成sc == rs,会破坏现有逻辑的简洁性。
简单来说,这种设计是为了让扩容线程的增减、扩容完成的判断逻辑更清晰自洽,避免不必要的状态混淆。
相关源码片段:
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)) // Start resizing transfer(tab, null); s = sumCount(); } }
内容的提问来源于stack exchange,提问作者user26615121

