ConcurrentHashMap的get方法为何无需加锁?volatile读与Node结构设计如何协同?

2026-04-24 17:162阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计849个文字,预计阅读时间需要4分钟。

ConcurrentHashMap的get方法为何无需加锁?volatile读与Node结构设计如何协同?

直接查看JDK 8的源码,`get(Object key)`方法从头到尾没有一行加锁代码。它依赖于底层内存模型约束能力,而非锁机制来保证正确性。

关键点在于:

volatile 修饰的 valnext 是可见性的核心

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;

这带来两个实际好处:

  • 构造完成后,hashkey 不会变,get 在比对 key 时不需要担心中途被其他线程篡改
  • 配合 volatile val,整个节点呈现一种“写后即稳”的状态:一旦节点被插入链表(通过 CAS 写入数组或 next),它的身份(key/hash)和值(val)就对所有读线程可见且稳定
  • 扩容时的 ForwardingNodeTreeBin 同样遵循这一设计原则,只是各自重写了 find(),但都不破坏读的无锁前提

扩容期间 get 仍能正确返回,靠的是 ForwardingNode 的桥接逻辑

扩容不是原子事件,新旧表并存一段时间。这时 get 遇到三种情况:

  • 桶位还是普通 Node → 正常遍历旧链表
  • 桶位是 ForwardingNodehash == MOVED)→ 调用它的 find()nextTable
  • 桶位是 TreeBinhash == TREEBIN)→ 进入红黑树查找,内部用读写锁保护结构变更,但不影响已有节点的 volatile

整个过程没有锁参与,也没有“查一半切表导致漏 key”的风险——因为每个 key 在迁移过程中只会存在于一个表中,且 ForwardingNode 是在该桶迁移完成之后才写入的,写入动作本身由 CAS 保证原子性。

真正容易被忽略的是 value 对象本身的线程安全性:ConcurrentHashMap 只管“把哪个引用放进去、让别人能读到这个引用”,至于这个引用指向的对象是否可变、是否需要额外同步,它一概不管。

标签:node

本文共计849个文字,预计阅读时间需要4分钟。

ConcurrentHashMap的get方法为何无需加锁?volatile读与Node结构设计如何协同?

直接查看JDK 8的源码,`get(Object key)`方法从头到尾没有一行加锁代码。它依赖于底层内存模型约束能力,而非锁机制来保证正确性。

关键点在于:

volatile 修饰的 valnext 是可见性的核心

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;

这带来两个实际好处:

  • 构造完成后,hashkey 不会变,get 在比对 key 时不需要担心中途被其他线程篡改
  • 配合 volatile val,整个节点呈现一种“写后即稳”的状态:一旦节点被插入链表(通过 CAS 写入数组或 next),它的身份(key/hash)和值(val)就对所有读线程可见且稳定
  • 扩容时的 ForwardingNodeTreeBin 同样遵循这一设计原则,只是各自重写了 find(),但都不破坏读的无锁前提

扩容期间 get 仍能正确返回,靠的是 ForwardingNode 的桥接逻辑

扩容不是原子事件,新旧表并存一段时间。这时 get 遇到三种情况:

  • 桶位还是普通 Node → 正常遍历旧链表
  • 桶位是 ForwardingNodehash == MOVED)→ 调用它的 find()nextTable
  • 桶位是 TreeBinhash == TREEBIN)→ 进入红黑树查找,内部用读写锁保护结构变更,但不影响已有节点的 volatile

整个过程没有锁参与,也没有“查一半切表导致漏 key”的风险——因为每个 key 在迁移过程中只会存在于一个表中,且 ForwardingNode 是在该桶迁移完成之后才写入的,写入动作本身由 CAS 保证原子性。

真正容易被忽略的是 value 对象本身的线程安全性:ConcurrentHashMap 只管“把哪个引用放进去、让别人能读到这个引用”,至于这个引用指向的对象是否可变、是否需要额外同步,它一概不管。

标签:node