深入分析ConcurrentHashMap(作用、原理、设计思想)

71 阅读5分钟

知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方可以评论,我们一起探讨!

1. 什么是 ConcurrentHashMap?

ConcurrentHashMap 是 Java 并发编程中用于高并发场景的线程安全哈希表实现,属于 java.util.concurrent 包。它通过优化锁粒度和数据结构设计,提供了高效的并发读写性能,解决了传统 HashtableCollections.synchronizedMap 的全局锁性能瓶颈问题。

2. 核心作用

  • 线程安全:支持多线程环境下的安全读写操作。
  • 高并发性能:通过分段锁(Java 7)或 CAS + 细粒度锁(Java 8)减少锁竞争。
  • 动态扩展:自动扩容哈希表,适应数据量变化。
  • 高效查询:Java 8 引入红黑树优化链表查询效率。

3. Java 7 和 Java 8 的对比分析

3.1 底层数据结构

Java 7:分段锁(Segment)
  • 结构设计
    • 将哈希表划分为多个段(Segment),每个段是一个独立的哈希表。
    • 每个段内部是 HashEntry 链表。
  • 示意图
    classDiagram
      class ConcurrentHashMap {
        <<Segment[] segments>>
      }
      class Segment {
        <<ReentrantLock>>
        HashEntry[] table
      }
      class HashEntry {
        int hash
        K key
        V value
        HashEntry next
      }
      ConcurrentHashMap --> Segment
      Segment --> HashEntry
    
Java 8:Node 数组 + 红黑树
  • 结构设计
    • 使用 Node 数组直接存储键值对。
    • 链表长度超过阈值(默认 8)时转换为红黑树。
  • 示意图
    classDiagram
      class ConcurrentHashMap {
        <<Node[] table>>
      }
      class Node {
        int hash
        K key
        V value
        Node next
      }
      class TreeNode {  
        TreeNode parent
        TreeNode left
        TreeNode right
        boolean red
      }
      ConcurrentHashMap --> Node
      Node --> TreeNode : 转换为红黑树
    

3.2 线程安全实现方式

Java 7:分段锁
  • 实现方式
    • 每个 Segment 继承自 ReentrantLock(当时synchronized还没有被优化),写操作时锁住对应段。
    • 读操作无锁(通过 volatile 保证可见性)。
  • 锁粒度:段级别(默认 16 段),不同段的操作可并发。
  • 示例流程put 操作):
    sequenceDiagram
      participant Client
      participant ConcurrentHashMap
      participant Segment
      participant HashEntry
    
      Client->>ConcurrentHashMap: put(key, value)
      ConcurrentHashMap->>ConcurrentHashMap: 计算哈希值
      ConcurrentHashMap->>Segment: 定位到段
      Segment->>Segment: lock()
      Segment->>HashEntry: 插入/更新链表
      Segment->>Segment: unlock()
      ConcurrentHashMap-->>Client: 完成
    
Java 8:CAS + 细粒度锁
  • 实现方式
    • CAS 操作:无锁化插入头节点(桶为空时)。
    • synchronized 锁:桶不为空时,锁住链表头节点(JDK1.6 以后 synchronized 锁做了很多优化)。
    • 红黑树优化:减少长链表查询时间。
  • 锁粒度:桶级别(单个 Node)。
  • 示例流程put 操作):
    sequenceDiagram
      participant Client
      participant ConcurrentHashMap
      participant Node
      participant TreeBin
    
      Client->>ConcurrentHashMap: put(key, value)
      ConcurrentHashMap->>ConcurrentHashMap: 计算哈希值
      ConcurrentHashMap->>Node: 定位到桶
      alt 桶为空
        ConcurrentHashMap->>Node: CAS 插入新节点
      else 桶非空
        ConcurrentHashMap->>Node: synchronized 锁住头节点
        Node->>Node: 遍历链表/树
        alt 链表长度 < 8
          Node->>Node: 插入链表
        else 链表长度 ≥ 8
          Node->>TreeBin: 转换为红黑树
          TreeBin->>TreeBin: 插入树节点
        end
        ConcurrentHashMap->>Node: 释放锁
      end
      ConcurrentHashMap-->>Client: 完成
    

3.3 加锁方式对比

特性Java 7Java 8
锁类型ReentrantLock(显式锁)synchronized(隐式锁)
锁粒度段级别(默认 16 段)桶级别(单个 Node
读操作无锁(通过 volatile无锁(通过 volatile + CAS
写操作锁住段锁住桶头节点或使用 CAS
数据结构优化链表链表 + 红黑树

4. 设计动机与优势

Java 7 的设计
  • 动机
    • 解决 Hashtable 全局锁的性能问题。
    • 通过分段锁减少锁竞争。
  • 优势
    • 读操作无锁,写操作仅锁住段。
    • 适合中等并发场景。
  • 缺点
    • 段数量固定,无法动态调整。
    • 内存开销较大(每个段独立维护结构)。
Java 8 的设计
  • 动机
    • 进一步减少锁竞争,提升高并发性能。
    • 解决长链表查询效率低的问题。
  • 优势
    • 更细粒度的锁:锁住单个桶,并发度更高。
    • 红黑树优化:链表查询复杂度从 O(n) 降至 O(log n)。
    • 动态扩容:更灵活的内存管理。
  • 缺点
    • 实现复杂度更高(需处理链表与树的转换)。

5. 关键改进总结

改进点Java 7Java 8
锁粒度段级别(粗粒度)桶级别(细粒度)
数据结构链表链表 + 红黑树
内存开销较高(每个段独立结构)较低(统一 Node 结构)
查询性能O(n)O(log n)(红黑树优化)
并发度依赖段数量(默认 16)更高(桶数量动态扩展)

6. 适用场景

  • Java 7
    • 中等并发场景,对内存开销不敏感。
    • 不需要处理极端长链表的情况。
  • Java 8
    • 高并发场景,需支持动态扩展。
    • 需优化长链表的查询性能。

7. 思考问题

  • ConcurrentHashMap的key和value为什么不可以为null?
    • 主要是为了让数据处理更清晰明确,避免这种难以区分的状况, 若 key 为 null ,就难以判断是这个键原本就不存在于 ConcurrentHashMap 中,还是仅仅是被设置为 null 。同理,若 value 为 null ,也无法明确是这个值真实存在于其中,还是因为找不到对应键而返回的 null
  • ConcurrentHashMap能保证复合操作的原子性吗?
    • 不能
    • 定义:复合操作是指由多个基本操作(如put、get、remove、containsKey等)组成的操作,例如先判断某个键是否存在containsKey(key),然后根据结果进行插入或更新put(key, value)。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。
  • 那如何保证 ConcurrentHashMap 复合操作的原子性呢?
    • ConcurrentHashMap 提供了一些原子性的复合操作(底层其实是CAS操作),如 putIfAbsent、compute、computeIfAbsent 、computeIfPresent、merge等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中。putIfAbsent:如果键之前不存在于映射中,返回 null,并将 key 和 value 插入到映射中。如果键已经存在,返回对应的值,不会更新映射中的值
    • 其实调用方法时加锁也是可以的,但是不建议,因为这样违背了使用 ConcurrentHashMap 的初衷。在使用 ConcurrentHashMap 的时候,尽量使用这些原子性的复合操作方法来保证原子性

总结

  • Java 7:通过分段锁实现并发控制,简单但扩展性有限。
  • Java 8:通过 CAS + 细粒度锁 + 红黑树,显著提升高并发性能和查询效率。
  • 设计演进的核心思想(都是分治思想)
    • 减少锁竞争:从段锁到桶锁,锁粒度更细。
    • 优化数据结构:引入红黑树解决链表性能问题。
    • 无锁化操作:利用 CAS 减少线程阻塞。