源码解析-ConcurrentHashMap

核心思想:分段锁与CAS

ConcurrentHashMap 的设计目标是在保证线程安全的同时,尽可能提高并发访问的效率。它的核心思想是减小锁的粒度,从 Hashtable 的全局锁进化到分段锁(JDK 7)再到 CAS + synchronized(JDK 8及以后)。

我们将以目前主流的 JDK 8+ 的实现为重点进行讲解。


第一部分:ConcurrentHashMap (JDK 8+) 源码详解

JDK 8 对 ConcurrentHashMap 进行了翻天覆地的重写,摒弃了分段锁,采用了与 HashMap 更相似的 Node数组+链表+红黑树结构,但通过大量使用 CAS操作对单个桶(链表头/树根)施加 synchronized 来实现并发控制。

1. 关键属性

// 核心数组,懒初始化,大小总是2的幂次方。
transient volatile Node<K,V>[] table;

// 在调整大小时使用的下一个表,非空时表示正在扩容。
private transient volatile Node<K,V>[] nextTable;

// 基础计数器,主要在没有竞争时使用,通过CAS更新。
private transient volatile long baseCount;

// 表初始化和扩容的控制标识符。
// 为负数:表正在初始化或扩容。-1表示初始化,-(1 + 活跃的扩容线程数)
// 为 0:创建时的初始值。
// 为正数:下一次扩容的阈值。
private transient volatile int sizeCtl;

sizeCtl 是一个极其重要的控制变量,它用单个变量通过不同的数值状态巧妙地控制了多种并发场景(初始化、扩容)。

2. put(K key, V value) 流程

put 方法是理解其并发设计的核心。它的流程可以概括为:CAS失败则重试,找到桶后就锁住这个桶进行操作

final V putVal(K key, V value, boolean onlyIfAbsent) {
  // 1. 参数校验,key和value不能为null (CHM不允许null键值)
  if (key == null || value == null) throw new NullPointerException();
  
  // 2. 计算key的散列值(通过spread方法,混合高低位,减少碰撞)
  int hash = spread(key.hashCode());
  int binCount = 0; // 记录链表长度,用于判断是否要树化

  // 3. 自旋(死循环),直到操作成功(插入、更新)才退出
  for (Node<K,V>[] tab = table;;) {
      Node<K,V> f; int n, i, fh;
      
      // 情况A:表还未初始化,则初始化表
      if (tab == null || (n = tab.length) == 0)
          tab = initTable(); // 使用CAS竞争初始化权
      
      // 情况B:计算出的桶(i)位置为空
      else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
          // 使用CAS操作,尝试将新Node放入这个空桶
          if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
              break; // CAS成功,插入完成,跳出循环
      }
      // 情况C:检测到该桶的节点的hash为MOVED,表示表正在扩容
      else if ((fh = f.hash) == MOVED)
          tab = helpTransfer(tab, f); // 当前线程协助扩容
      
      // 情况D:桶不为空,且不在扩容状态,则锁定这个桶(头节点f)
      else {
          V oldVal = null;
          synchronized (f) { // 对桶的头节点加锁,粒度非常细
              if (tabAt(tab, i) == f) { // 再次验证头节点没被改变(双检锁思想)
                  if (fh >= 0) { // 普通链表节点
                      binCount = 1;
                      // 遍历链表...
                      for (Node<K,V> e = f;; ++binCount) {
                          // 找到key,则更新value
                          // 没找到,则在链表尾部添加新Node
                      }
                  }
                  else if (f instanceof TreeBin) { // 红黑树节点
                      // 在红黑树中执行插入操作...
                  }
              }
          }
          // 锁释放后,判断是否需要将链表转为红黑树
          if (binCount != 0) {
              if (binCount >= TREEIFY_THRESHOLD)
                  treeifyBin(tab, i);
              if (oldVal != null)
                  return oldVal;
              break;
          }
      }
  }
  // 4. 更新元素计数,并判断是否需要扩容
  addCount(1L, binCount);
  return null;
}


核心要点:

  • 无锁化尝试:遇到空桶时,使用 CAS 无锁操作进行插入,这是最高效的方式。
  • 细粒度锁:遇到哈希冲突时,只对单个桶(链表或树的头节点)进行 synchronized 加锁。这意味着可以同时有16个(默认容量)甚至更多的线程同时进行 put 操作,只要它们操作的桶不同。
  • 协助扩容 (Helping Transfer):如果线程发现当前表正在扩容 (fh == MOVED),它不会阻塞等待,而是主动参与帮助数据迁移,加快扩容过程。这是一种“化敌为友”的巧妙设计。
  • CAS控制初始化与计数:表的初始化和元素总数的更新 (addCount) 都通过CAS操作完成,避免了全局锁。

3. get(Object key) 流程

get 操作是完全无锁的,这也是它高性能的关键。

public V get(Object key) {
  Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
  int h = spread(key.hashCode());
  if ((tab = table) != null && (n = tab.length) > 0 &&
      (e = tabAt(tab, (n - 1) & h)) != null) { // volatile读获取头节点
      if ((eh = e.hash) == h) { // 如果头节点就是要找的
          if ((ek = e.key) == key || (ek != null && key.equals(ek)))
              return e.val;
      }
      // 如果hash为负,说明是特殊节点(正在扩容或是树),调用对应的find方法
      else if (eh < 0)
          return (p = e.find(h, key)) != null ? p.val : null;
      // 遍历链表
      while ((e = e.next) != null) {
          if (e.hash == h &&
              ((ek = e.key) == key || (ek != null && key.equals(ek))))
              return e.val;
      }
  }
  return null;
}

核心要点:

  • 无锁:整个过程没有使用任何锁,仅通过 volatileUNSAFE 提供的原子性 volatile读 (tabAt) 来保证每次读到的是最新写入的值。
  • 弱一致性:由于无锁,get 操作可能无法完全反映最近某个 put 操作的结果(比如正在遍历的链表下一刻被另一个线程修改了),这是为了性能而做的妥协,符合 ConcurrentHashMap弱一致性迭代器语义。但它能保证最终能读到某个已完成操作的结果。

第二部分:ConcurrentHashMap vs. HashMap

特性HashMap (非线程安全)ConcurrentHashMap (线程安全)
线程安全。多线程下使用会导致数据错乱,如死循环(JDK7)、数据丢失等。。采用CAS + synchronized 实现高效并发。
Null 键/值允许。可以有一个null键和多个null值。不允许put(null, ...) 会直接抛出 NullPointerException
迭代器Fail-Fast。在迭代过程中如果结构被修改(除了Iterator.remove),会立即抛出 ConcurrentModificationExceptionWeakly Consistent (弱一致性)。迭代器反映创建时或之后的某个状态,可以安全地在迭代时进行并发修改,不会抛异常。
性能单线程下最高。无任何同步开销。。并发环境下性能远高于 HashtableCollections.synchronizedMap。读操作完全无锁,写操作锁粒度很细。
底层实现 (JDK8+)数组 + 链表 + 红黑树数组 + 链表 + 红黑树
锁的机制无锁CAS + synchronized。锁的粒度是单个桶(链表头节点/树根)。
扩容单线程操作,执行期间所有操作阻塞。多线程协同扩容。当前线程插入时发现正在扩容,会帮助迁移数据。
继承体系继承自 AbstractMap继承自 AbstractMap,但实现了 ConcurrentMap 接口。

总结

  • HashMap 是高效的非线程安全散列表,适用于单线程环境或作为局部变量。
  • ConcurrentHashMapHashMap线程安全版本,但其实现并非简单粗暴地加锁。它通过 CAS无锁算法细粒度的同步锁(synchronized,将锁的范围从整个表缩小到单个桶,极大地提升了并发性能。其协助扩容机制更是精妙,将并发操作的瓶颈转化为优势。

简单来说,ConcurrentHashMap 的设计是 “读操作完全无锁,写操作冲突时才最小化加锁” 这一并发设计哲学的经典体现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值