You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JDK8 ConcurrentHashMap中counterCells为何未用volatile元素访问方法

JDK8 ConcurrentHashMap volatile数组元素访问差异解析

JDK8版本的ConcurrentHashMap使用volatile修饰的Node<K,V>[] table作为哈希表核心存储结构。由于volatile修饰数组时,仅能保证数组引用本身的可见性,数组内元素不天然具备volatile语义,因此源码专门提供了三个借助Unsafe实现的、带volatile语义的数组元素访问方法:

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);
}

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);
}

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

但源码中另一个volatile修饰的数组volatile CounterCell[] counterCells,访问元素时完全没有使用上述volatile语义逻辑,相关代码片段如下:

if ((as = counterCells) != null && (n = as.length) > 0) {
    if ((a = as[(n - 1) & h]) == null) { // 此处as[(n - 1) & h]未使用volatile访问
        if (cellsBusy == 0) {            // 尝试挂载新的Cell
            CounterCell r = new CounterCell(x); // 乐观创建
            if (cellsBusy == 0 &&
                U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean created = false;
                try {               // 锁状态下二次检查
                    CounterCell[] rs; int m, j;
                    if ((rs = counterCells) != null &&
                        (m = rs.length) > 0 &&
                        rs[j = (m - 1) & h] == null) { //此处rs[j = (m - 1) & h]未使用volatile
                        rs[j] = r; // 此处赋值未使用volatile语义
                        created = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (created)
                    break;
                continue;           // 槽位已非空,重试
            }
        }
        collide = false;
    }
    // 其余逻辑省略
}

为什么table数组需要volatile元素访问,counterCells不需要

核心差异来自两个数组的并发访问场景和一致性要求完全不同:

  • 对table数组来说,它是存储核心键值对的主结构,ConcurrentHashMap的get()操作是完全无锁的,既不加内置锁也不依赖其他同步变量,全程直接读数组。如果元素访问不加volatile语义,很可能出现某个线程已经把节点放进数组槽位,其他读线程长时间看不到新值,甚至读到半初始化的对象引用,直接破坏并发安全。此外table的节点修改、扩容迁移等操作大量依赖无锁CAS实现,volatile读是CAS正常工作的前提——如果拿不到槽位的最新值作为CAS的预期参数,CAS要么反复失败要么出现ABA问题,根本无法正常运行。
  • 对counterCells数组来说,它是高并发场景下优化计数性能的辅助结构,访问逻辑全程有同步机制兜底,根本不需要单独给数组元素加volatile语义:
    • 所有往数组里新增CounterCell的写操作,都必须先通过CAS抢到cellsBusy这个volatile修饰的锁,CAS操作本身自带全内存屏障,写完释放锁(将cellsBusy改回0)的volatile写,会自动把当前线程对数组元素的修改全部刷回主存,不需要单独对数组元素做volatile写。
    • 读数组元素的场景只有两种:要么是已经拿到cellsBusy锁的线程,锁的happens-before规则天然保证它能看到之前所有的修改;要么是计数累加时的乐观读,就算读到旧的null值也不会影响正确性,最多走一遍抢锁、重试累加的流程。本来counterCells的设计思路就是乐观试错、减少锁竞争,本身就允许读的时候出现小概率旧值,最终计数正确性靠重试和锁兜底即可,没必要用开销更高的volatile读。
    • 至于CounterCell内部存储计数值的value字段,本身修改就是靠CAS完成的,CAS自带的内存屏障已经保证了value的可见性,和数组槽位本身的访问语义没有关系。

volatile数组元素使用volatile访问的适用场景

首先要明确一个基础规则:volatile修饰数组时,仅保证数组引用本身的可见性,数组元素的可见性需要靠访问逻辑自行保证。只要满足以下任意一个条件,就必须通过Unsafe的volatile系列方法、或者VarHandle的volatile访问模式操作数组元素:

  • 数组元素的读操作完全无锁,也没有其他同步手段(比如锁、其他volatile变量的读写)提供内存屏障,且业务要求必须读到元素的最新值,不能容忍长时间的可见性延迟。
  • 需要对数组元素执行CAS操作:CAS的执行前提是先拿到元素的当前最新值作为预期参数,不使用volatile读就无法稳定拿到最新值,CAS逻辑会完全失效。
  • 数组元素的写入没有锁或者其他volatile写的内存屏障配合,要求写入的结果立刻对其他线程可见。
    反过来,如果数组的所有读写操作都在锁的保护范围内,或者访问逻辑允许读到小概率旧值(可以通过后续重试、兜底校验修正结果),就完全没必要给元素加volatile访问,可以省下volatile读写带来的内存屏障开销。

内容的提问来源于stack exchange,提问作者asiuf爱施德

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 11:15:54