惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
月光博客
月光博客
雷峰网
雷峰网
WordPress大学
WordPress大学
博客园 - 司徒正美
Last Week in AI
Last Week in AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Visual Studio Blog
H
Help Net Security
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
S
Security @ Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
爱范儿
爱范儿
W
WeLiveSecurity
J
Java Code Geeks
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
T
Threatpost
The Cloudflare Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
N
Netflix TechBlog - Medium
Latest news
Latest news
V2EX - 技术
V2EX - 技术
小众软件
小众软件
T
The Blog of Author Tim Ferriss
A
Arctic Wolf
B
Blog RSS Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
I
InfoQ
C
Check Point Blog
N
News | PayPal Newsroom
Cyberwarzone
Cyberwarzone
V
V2EX
TaoSecurity Blog
TaoSecurity Blog
P
Privacy & Cybersecurity Law Blog
Microsoft Security Blog
Microsoft Security Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
D
DataBreaches.Net
F
Fortinet All Blogs
阮一峰的网络日志
阮一峰的网络日志
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
IT之家
IT之家
K
Kaspersky official blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google DeepMind News
Google DeepMind News
C
CXSECURITY Database RSS Feed - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com

博客园 - 郭慕荣

彻底搞懂 Codex Skill 与插件:区别、试用、核心作用、项目落地全指南(程序员实战向) Codex Skill 和插件到底有什么区别?程序员如何在项目中落地使用 claudecode 配置防止配置文件泄露总结 Claude code分析外部文件总结 Claude code三种模式详解 hasCompletedOnboarding详解 Claudecode 有哪些常用的命令 claude.md文件详解 claude code 的setting文件详解 Docker部署zookeeper总结 Mac中git ssh的配置(GitLab) Java 类加载机制 面试题(一) 如何监控和调优JVM性能? jvm常用的参数有哪些? 怎么配置? aop在项目中使用的场景?怎么使用? Java dubbo spring springboot中的spi机制 spring中常见的两种代理模式 Redis的zset 面试汇总 drools 规则引擎在线化配置 springcloud中常用的注解详解 springcloud中网关gateway总结 spring是怎么解决循环依赖的? MySQL 死锁 怎么处理? 在写left join的时候 是大表在左侧 还是小表在左侧(二) 在写left join的时候 是大表在左侧 还是小表在左侧(一) nacos客户端(接口调用者)如何感知被调用服务下线? (二) nacos客户端(接口调用者)如何感知被调用服务下线?(一) 在MySQL中 redolog undolog binlog 写入的场景,顺序
hashmap 和currenthashmap 的原理?详解一下
郭慕荣 · 2025-10-29 · via 博客园 - 郭慕荣

HashMap 和 ConcurrentHashMap 是 Java 中用于存储键值对(Key-Value)的哈希表实现,但前者是非线程安全的,后者是线程安全的,且两者的底层原理和设计目标有显著差异。以下从数据结构、核心机制、线程安全(仅 ConcurrentHashMap) 三个维度详细解析。

HashMap 是 Java 中最常用的哈希表实现,用于快速存储和查询键值对,核心目标是高效的增删改查,不保证线程安全。

JDK 1.8 及之后,HashMap 的底层结构为 “数组(哈希桶)+ 链表 + 红黑树” 的复合结构:

  • 数组(哈希桶,Node[] table):数组的每个元素称为 “桶”,用于存储键值对的首节点。数组长度(容量,capacity)默认是 16,且始终保持为 2 的幂次方(如 16、32、64...),这是为了通过位运算快速计算元素位置。
  • 链表(Node 节点):当多个键的哈希值映射到同一个桶时,会形成链表(解决哈希冲突)。每个 Node 包含 hash(键的哈希值)、keyvaluenext(下一个节点引用)。
  • 红黑树(TreeNode 节点):当链表长度超过阈值(默认 8),且数组容量 ≥ 64 时,链表会转为红黑树(一种自平衡二叉查找树)。红黑树的查询时间复杂度为 O (log n),远优于链表的 O (n),可优化大量哈希冲突时的查询效率。

HashMap 通过键(Key)的哈希值确定元素在数组中的位置,步骤如下:

  1. 计算键的哈希值:int hash = hash(key),其中 hash(key) 是对 key.hashCode() 的二次哈希(通过高位异或低位,减少哈希冲突):
    static final int hash(Object key) {
        int h;
        
  2. 计算数组索引:int index = (n - 1) & hash,其中 n 是数组容量(table.length)。由于 n 是 2 的幂次方,n-1 的二进制全为 1(如 16-1=15 → 1111),& 运算等价于取模(hash % n),但效率更高。

put(key, value) 用于插入键值对,步骤如下:

  1. 若数组 table 未初始化或长度为 0,先触发扩容(resize())。
  2. 计算 key 的 hash 和 index,检查索引 index 处的桶是否为空:
    • 若为空,直接创建 Node 插入(table[index] = newNode(hash, key, value, null))。
    • 若不为空(哈希冲突):
      • 若桶的首节点与 key 相同(hash 相等且 key.equals() 为 true),直接替换 value
      • 若首节点是红黑树节点(TreeNode),调用树的插入方法(putTreeVal)。
      • 若首节点是链表节点,遍历链表:
        • 若找到相同 key,替换 value
        • 若未找到,在链表尾部插入新节点,插入后若链表长度 ≥ 8,触发链表转红黑树(treeifyBin)。
  3. 插入后若元素数量(size)超过阈值(threshold = capacity * loadFactor,默认负载因子 loadFactor=0.75),触发扩容(resize())。

当元素数量超过阈值时,HashMap 会扩容(容量翻倍),目的是减少哈希冲突,步骤如下:

  1. 计算新容量:newCap = oldCap << 1(原容量 × 2,保持 2 的幂次方)。
  2. 创建新数组(newTable),容量为 newCap
  3. 将原数组(oldTable)中的元素迁移到新数组:
    • 对于链表 / 红黑树中的每个节点,重新计算在新数组中的索引(因 newCap 是原容量的 2 倍,新索引要么是原索引,要么是 原索引 + oldCap,无需重新计算 hash,通过 hash & oldCap 判断高位是否为 1 即可)。
    • 若原节点是链表,迁移时可能拆分链表为两个子链表(分别对应新索引的两个位置);若原节点是红黑树,迁移后可能退化为链表(若元素数量 < 6)。
  4. 替换 table 为 newTable,更新阈值(newThr = newCap * loadFactor)。

HashMap 未做任何同步处理,多线程环境下操作可能导致问题:

  • 扩容死循环(JDK 1.7 及之前):多线程同时扩容时,链表迁移可能形成环形链表,导致后续查询时无限循环。
  • 数据丢失:多线程同时 put 时,可能覆盖彼此的插入结果。
  • 数据不一致:size 计数未加锁,可能导致统计结果错误。

ConcurrentHashMap 是线程安全的哈希表实现,专为多线程环境设计,支持高并发的增删改查,同时尽量减少锁竞争带来的性能损耗。其设计在 JDK 1.7 和 1.8 有较大差异,这里以更常用的 JDK 1.8+ 为主讲解。

JDK 1.8 摒弃了 JDK 1.7 的 “分段锁(Segment)” 机制,底层结构与 HashMap 类似:“数组(哈希桶)+ 链表 + 红黑树”,但通过更细粒度的同步机制保证线程安全。

JDK 1.8 采用 “无锁 CAS 尝试 + synchronized 细粒度锁” 保证线程安全,避免了 JDK 1.7 分段锁的粗粒度竞争:

  • CAS(Compare-And-Swap):对于未初始化的桶(table[index] == null),通过 CAS 原子操作尝试插入新节点(casTabAt 方法),避免加锁。
  • synchronized 锁:若 CAS 失败(桶已被其他线程占用,存在哈希冲突),则对桶的首节点(table[index])加 synchronized 锁,仅锁定当前冲突的桶,其他桶仍可被并发访问,锁粒度更细。

put(key, value) 步骤与 HashMap 类似,但增加了同步处理:

  1. 计算 key 的 hash(与 HashMap 相同),若数组未初始化,先通过 CAS 初始化(initTable)。
  2. 定位索引 index,检查桶是否为空:
    • 若为空,通过 casTabAt 方法(CAS)尝试插入新节点,成功则返回;失败(被其他线程抢先插入)则重试。
    • 若不为空(存在哈希冲突):
      • 若桶的首节点是 MOVED 状态(正在扩容),当前线程协助扩容(helpTransfer),避免扩容线程负载过重。
      • 否则,对首节点加 synchronized 锁,遍历链表 / 红黑树:
        • 若找到相同 key,替换 value(通过 cas 保证原子性)。
        • 若未找到,插入新节点(链表尾部或红黑树),插入后检查是否需要转红黑树。
  3. 插入后更新 size(通过 addCount 方法,CAS 累加,超过阈值则触发扩容)。

ConcurrentHashMap 的扩容支持多线程协同,避免单线程扩容效率低:

  1. 当 size 超过阈值时,由某个线程发起扩容(transfer 方法),将数组容量翻倍。
  2. 扩容时,原数组(oldTab)被分为多个段,每个线程负责迁移一段(通过 stride 控制,默认每个线程处理 16 个桶)。
  3. 其他线程执行 put/get 时,若发现正在扩容(桶的首节点为 MOVED),会协助迁移当前桶的元素,加速扩容过程。
  4. 迁移逻辑与 HashMap 类似(新索引 = 原索引 或 原索引 + oldCap),但通过 CAS 标记迁移状态,避免冲突。

get(key) 操作无需加锁,直接通过哈希定位桶,遍历链表 / 红黑树查找,原因是:

  • 节点的 value 用 volatile 修饰,保证多线程间的可见性(修改后立即刷新到主内存,读取时从主内存加载)。
  • 红黑树的结构修改(如插入、删除)通过 synchronized 锁保证原子性,读取时无需锁即可获取最新结构。
  • HashMap 是单线程场景下的高效哈希表,底层为 “数组 + 链表 + 红黑树”,通过哈希散列和扩容减少冲突,非线程安全。
  • ConcurrentHashMap 是多线程场景下的线程安全实现,JDK 1.8 用 “CAS + synchronized” 保证安全,锁粒度细,并发性能优异,底层结构与 HashMap 类似,但不支持 null 键值,迭代器为弱一致性。

实际开发中,单线程环境用 HashMap,多线程环境(如并发写入)用 ConcurrentHashMap