ConcurrentHashMap的get方法为何无需加锁?volatile读与Node结构设计如何协同?
- 内容介绍
- 文章标签
- 相关推荐
本文共计849个文字,预计阅读时间需要4分钟。
直接查看JDK 8的源码,`get(Object key)`方法从头到尾没有一行加锁代码。它依赖于底层内存模型约束能力,而非锁机制来保证正确性。
关键点在于:
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 的:
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 只管“把哪个引用放进去、让别人能读到这个引用”,至于这个引用指向的对象是否可变、是否需要额外同步,它一概不管。
本文共计849个文字,预计阅读时间需要4分钟。
直接查看JDK 8的源码,`get(Object key)`方法从头到尾没有一行加锁代码。它依赖于底层内存模型约束能力,而非锁机制来保证正确性。
关键点在于:
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 的:
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 只管“把哪个引用放进去、让别人能读到这个引用”,至于这个引用指向的对象是否可变、是否需要额外同步,它一概不管。

