并发 - ConcurrentHashMap 深度解析

ConcurrentHashMap 深度解析

ConcurrentHashMap是Java并发包(JUC)中最重要的线程安全集合之一,也是面试中关于并发编程的最高频考点。

1. 为什么需要 ConcurrentHashMap?

HashMap 是非线程安全的,在并发环境下使用会导致数据覆盖甚至死循环。虽然可以通过Hashtable或Collections.synchronizedMap(new HashMap<>())来实现线程安全的Map,但这两种方案都是通过对整个Map对象加synchronized重锁来实现的,锁的粒度太大,导致所有操作都必须串行执行,性能极差。

ConcurrentHashMap 的出现就是为了在保证线程安全的同时,提供尽可能高的并发性能。


2. JDK 1.7 vs. JDK 1.8 的实现原理对比

a) JDK 1.7: 分段锁 (Segment Locking)

  • 核心思想: 将整个哈希表在逻辑上划分为多个Segment(默认16个)。每个Segment本身就像一个小型的、线程安全的HashMap,它继承自ReentrantLock,拥有自己独立的锁。
  • 数据结构: ConcurrentHashMap -> Segment[] -> HashEntry[] -> HashEntry 链表。
  • 工作方式:
    • 当一个线程需要对某个数据进行操作时,它只需要锁定该数据所属的那个Segment,而不会影响其他Segment的操作。
    • get操作大部分情况下不加锁,通过volatile保证可见性。
  • 优点: 相比于对整个Map加锁,分段锁技术允许多个线程同时访问不同的Segment,并发度大大提高(理论上最高并发度就是Segment的数量)。
  • 缺点: 锁的粒度还是比较大(锁住了整个Segment),在数据分布不均匀或高竞争的情况下,仍然可能出现热点Segment的性能瓶瓶颈。

b) JDK 1.8: CAS + synchronized

  • 核心思想: 放弃了Segment,回归到“数组+链表/红黑树”的基础结构上,通过极大地减小锁的粒度来提高并发性能。
  • 数据结构: Node[] -> Node链表 / TreeNode红黑树。
  • put操作的线程安全机制:
    1. 初始化:如果Node[]数组(table)为空,通过CAS进行初始化,保证只有一个线程能成功。
    2. 桶为空:如果要插入的位置(桶)是空的,直接使用CAS尝试写入新节点,这是无锁的。如果CAS失败,说明有竞争,则进入下一步。
    3. 桶不为空(发生哈希冲突):
      • 如果桶的头节点是链表节点,则使用synchronized锁住该桶的头节点,然后再进行链表遍历和插入操作。
      • 如果桶的头节点是红黑树节点,也同样使用synchronized锁住头节点。
  • 优点: 将锁的粒度从“段(Segment)”缩小到了“桶的头节点”,只有在操作同一个哈希桶时才会发生竞争,极大地提高了高竞争下的并发性能。
  • synchronized锁优化: JDK 1.8中,JVM对synchronized进行了大量优化(偏向锁、轻量级锁、自旋),对于ConcurrentHashMap这种锁粒度小、占用时间短的场景,其性能已经不输于甚至优于ReentrantLock。

3. 核心问题深度解析

a) size() 方法为什么不精确?

  • ConcurrentHashMap的size()方法返回的是一个估计值,而不是一个实时精确的值。
  • 原因: 为了保证高性能,size()方法在计算元素总数时不会对整个Map进行加锁。
  • 实现机制 (JDK 1.8):
    1. ConcurrentHashMap内部维护一个baseCount变量和一个CounterCell[]数组来分散计数。
    2. put/remove操作会通过CAS来更新这些计数值。
    3. size()方法会累加baseCount和所有CounterCell的值。
    4. 由于在size()方法进行累加的过程中,其他线程可能同时在进行put或remove操作,这些操作对计数值的修改可能不会立即被size()方法观察到,因此size()方法返回的是一个估计值。
  • 结论: 这是一个为了高性能和高并发而对数据一致性做出的权衡。如果需要精确的数量(如mappingCount()方法),在Java 8中虽然比size()更接近精确,但仍然不保证是原子性的,如果需要绝对精确,仍然需要外部加锁。

b) 迭代器为什么不会抛出 ConcurrentModificationException?

  • 答案: 不会。
  • 原因: ConcurrentHashMap的迭代器被设计为“弱一致性(Weakly Consistent)”或“故障安全(Fail-Safe)”的。
  • 工作机制:
    • 当你创建一个迭代器时,它反映的是ConcurrentHashMap在某个时间点的状态的快照。
    • 在迭代过程中,如果其他线程对Map进行了修改(增、删),迭代器不保证能反映出这些修改,但它也绝不会抛出ConcurrentModificationException。
    • 迭代器看到的内容,是基于创建它那一刻的哈希表结构,并且可能会(但不保证)反映出后续的一些修改。
  • 对比: 这与HashMap或ArrayList的“快速失败(Fail-Fast)”迭代器形成鲜明对比,后者在迭代时如果发现集合被修改,会立即抛出ConcurrentModificationException。
posted @ 2026-01-21 15:59  我是刘瘦瘦  阅读(251)  评论(0)    收藏  举报