ConcurrentHashMap 的 get 方法无锁,靠 volatile 和 final 保证可见性与稳定性:val 和 next 为 volatile,确保读到最新值;hash 和 key 为 final,构造后不可变;扩容时通过 ForwardingNode 桥接新旧表,find() 保障正确查找。

get 方法里根本没出现 synchronized 或 lock()
直接看 JDK 8 的源码,get(Object key) 方法从头到尾没有一行加锁代码。它靠的是底层内存模型约束,而不是锁机制来保证正确性。关键点在于:读操作只做查找,不改结构;而所有可能被并发修改的字段,都用 volatile 或 final 修饰,确保线程能立即看到最新值。
volatile 修饰的 val 和 next 是可见性的核心
ConcurrentHashMap 的节点类 Node 中,这两个字段声明是:
volatile V val; volatile Node<K,V> next;
这意味着:
- 任何线程对
val的写(比如put成功后设置值),其他线程调用get时一定能读到最新值,不会因为 CPU 缓存不一致而读到旧值 -
next字段的volatile语义,让链表遍历过程中的“跳转”动作具有顺序一致性——不会出现读到一个非空next,但其内容却是未初始化的状态 - 注意:
val是引用类型,volatile只保证引用本身的可见性,不保证其指向对象内部状态的线程安全(比如val是个可变的ArrayList,那还得自己同步)
Node 的不可变性让读操作天然无干扰
Node 类的关键字段是 final 的:
生成Blender Python (bpy)脚本,该脚本使用node_groups API和GeometryNodeTree接口程序化地创建几何节点修改器树,用于参数化3D资产生成。
final int hash; final K key;
这带来两个实际好处:
- 构造完成后,
hash和key不会变,get在比对key时不需要担心中途被其他线程篡改 - 配合
volatile val,整个节点呈现一种“写后即稳”的状态:一旦节点被插入链表(通过 CAS 写入数组或next),它的身份(key/hash)和值(val)就对所有读线程可见且稳定 - 扩容时的
ForwardingNode和TreeBin同样遵循这一设计原则,只是各自重写了find(),但都不破坏读的无锁前提
扩容期间 get 仍能正确返回,靠的是 ForwardingNode 的桥接逻辑
扩容不是原子事件,新旧表并存一段时间。这时 get 遇到三种情况:
- 桶位还是普通
Node→ 正常遍历旧链表 - 桶位是
ForwardingNode(hash == MOVED)→ 调用它的find()去nextTable查 - 桶位是
TreeBin(hash == TREEBIN)→ 进入红黑树查找,内部用读写锁保护结构变更,但不影响已有节点的volatile读
整个过程没有锁参与,也没有“查一半切表导致漏 key”的风险——因为每个 key 在迁移过程中只会存在于一个表中,且 ForwardingNode 是在该桶迁移完成之后才写入的,写入动作本身由 CAS 保证原子性。
真正容易被忽略的是 value 对象本身的线程安全性:ConcurrentHashMap 只管“把哪个引用放进去、让别人能读到这个引用”,至于这个引用指向的对象是否可变、是否需要额外同步,它一概不管。

















