ConcurrentHashMap 原理详解

ConcurrentHashMap 原理详解

一、概述

ConcurrentHashMap 是 Java 并发包(java.util.concurrent)中提供的线程安全的哈希表实现,专为高并发场景设计。它解决了 HashMap 在并发环境下的线程安全问题,同时相比 Hashtable 提供了更高的并发性能。

ConcurrentHashMap 的核心设计目标是在保证线程安全的前提下,尽可能提高并发访问效率,主要通过分段锁(JDK7)或CAS+synchronized(JDK8)机制实现。

二、JDK7 中的 ConcurrentHashMap 原理

2.1 数据结构

JDK7 的 ConcurrentHashMap 采用 "分段数组 + HashEntry 链表" 的结构:

  • Segment 数组:本质是一个可重入锁(ReentrantLock),每个 Segment 包含一个 HashEntry 数组
  • HashEntry 数组:存储实际的键值对,每个 HashEntry 是一个链表节点
  • 分段锁机制:每个 Segment 独立加锁,不同 Segment 上的操作可以并发执行

结构示意图:

ConcurrentHashMap

┌─────────────────────────────────────────────────────┐

│ segments: Segment[] │

│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ... │

│ │Segment 0│ │Segment 1│ │Segment 2│ │

│ │┌───────┐│ │┌───────┐│ │┌───────┐│ │

│ ││Entry[]││ ││Entry[]││ ││Entry[]││ │

│ │└───────┘│ │└───────┘│ │└───────┘│ │

│ └─────────┘ └─────────┘ └─────────┘ │

└─────────────────────────────────────────────────────┘

2.2 核心原理

  1. 分段锁机制
    • 整个哈希表被分为多个 Segment(默认 16 个)
    • 操作某段数据时,仅对该段加锁,其他段可并发访问
    • 理论上,并发度为 Segment 的数量(默认 16)
  2. 定位元素流程
    • 计算 key 的哈希值
    • 确定该 key 属于哪个 Segment(hash >>> segmentShift & segmentMask)
    • 在对应 Segment 中计算 HashEntry 数组的索引
    • 操作对应位置的链表
  3. get 操作
    • 无需加锁,通过 volatile 关键字保证可见性
    • 遍历链表查找元素,若期间发生修改,volatile 保证能看到最新值
  4. put 操作
    • 计算 Segment 位置并获取该 Segment 的锁
    • 在 Segment 内部执行类似 HashMap 的 put 操作
    • 操作完成后释放锁
    • 若 Segment 中元素数量超过阈值,仅对该 Segment 进行扩容
  5. size 操作
    • 先尝试无锁方式计算总大小(最多重试 3 次)
    • 若期间发生修改,对所有 Segment 加锁后再计算

三、JDK8 中的 ConcurrentHashMap 原理

3.1 数据结构

JDK8 对 ConcurrentHashMap 进行了重大重构,采用与 HashMap 类似的 "数组 + 链表 + 红黑树" 结构,但通过更细粒度的同步机制实现线程安全:

  • 取消了 Segment 分段锁,直接使用 Node 数组存储数据
  • 引入红黑树优化长链表(阈值 8)
  • 使用 CAS 操作和 synchronized 关键字实现同步

结构示意图:

ConcurrentHashMap

┌─────────────────────────────────────────────────────┐

│ table: Node[] │

│ ┌───────┐ ┌───────┐ ┌───────┐ ... │

│ │Node │ │Node │ │TreeNode│ │

│ │(链表) │ │(链表) │ │(红黑树)│ │

│ └───────┘ └───────┘ └───────┘ │

└─────────────────────────────────────────────────────┘

3.2 核心原理

  1. 同步机制
    • 采用CAS + synchronized组合实现线程安全
    • 对链表头节点或红黑树根节点使用 synchronized 加锁
    • 粒度更细,仅锁定冲突的链表或红黑树,而非整个分段
  2. 关键内部类
    • Node:基础节点,存储键值对,value 和 next 使用 volatile 修饰
    • TreeNode:红黑树节点,继承自 Node
    • ForwardingNode:扩容时使用的节点,标记当前节点已迁移
  3. get 操作
    • 无锁操作,通过 volatile 保证可见性
    • 计算索引位置,遍历链表或红黑树查找元素
    • 若遇到 ForwardingNode,说明正在扩容,会到新表中查找
  4. put 操作

1. 计算key的哈希值

2. 若数组未初始化,初始化数组

3. 计算索引位置,获取该位置的节点f

4. 若f为null,尝试用CAS插入新节点

5. 若f为ForwardingNode,说明正在扩容,帮助迁移并重新尝试put

6. 否则,对f加synchronized锁:

a. 遍历链表查找是否存在相同key

b. 存在则替换value,不存在则插入新节点

c. 检查是否需要将链表转为红黑树

7. 若插入了新节点,检查是否需要扩容

  1. 扩容机制
    • 支持多线程并发扩容
    • 扩容时设置 sizeCtl 为负数值标记扩容状态
    • 每个线程负责迁移一部分节点
    • 使用 ForwardingNode 标记已迁移的桶
  2. size 操作
    • 维护 baseCount 和 counterCells 两个变量
    • 并发更新时通过 CAS 更新 counterCells
    • 计算总大小时合并 baseCount 和 counterCells 的值

四、JDK7 与 JDK8 的 ConcurrentHashMap 对比

特性

JDK7

JDK8

数据结构

Segment 数组 + HashEntry 链表

Node 数组 + 链表 + 红黑树

同步机制

基于 ReentrantLock 的分段锁

CAS + synchronized

并发度

由 Segment 数量决定(默认 16)

理论上与数组长度相当

锁粒度

Segment 级别

链表 / 红黑树级别

扩容

单个 Segment 独立扩容

全表扩容,支持多线程协作

红黑树支持

不支持

支持

内存占用

较高(多一层 Segment)

较低

性能

中等

高(细粒度锁)

五、ConcurrentHashMap 的优势

  1. 高并发性能
    • 细粒度的同步机制,允许多个线程同时访问不同的桶
    • 读操作无锁,性能接近 HashMap
  2. 线程安全保证
    • 所有修改操作都是线程安全的
    • 提供了弱一致性的迭代器,不会抛出 ConcurrentModificationException
  3. 丰富的原子操作
    • 提供 putIfAbsent、remove、replace 等原子操作
    • 避免了手动加锁实现原子操作的繁琐
  4. 内存可见性
    • 通过 volatile 关键字保证节点的可见性
    • 确保一个线程的修改能被其他线程看到

六、适用场景

  • 高并发环境下的键值对存储
  • 缓存系统实现
  • 多线程共享数据的场景
  • 需要频繁进行读操作,同时有一定写操作的场景

相比 Hashtable(全表锁)和 Collections.synchronizedMap(全表锁),ConcurrentHashMap 在高并发场景下具有明显的性能优势,是线程安全哈希表的首选。

posted @ 2025-09-02 15:24  诸葛匹夫  阅读(149)  评论(0)    收藏  举报