0

0

HashMap 的底层实现原理是怎样的?(基于JDK 8)

betcha

betcha

发布时间:2025-09-03 23:31:02

|

167人浏览过

|

来源于php中文网

原创

答案:jdk 8中hashmap采用“数组+链表/红黑树”结构,通过扰动哈希值并按位与确定索引,冲突时链表存储,链表长度≥8且容量≥64时转为红黑树;扩容时容量翻倍并再哈希,多线程不安全,推荐使用concurrenthashmap。

hashmap 的底层实现原理是怎样的?(基于jdk 8)

HashMap在JDK 8中的底层实现,核心是“数组+链表/红黑树”的混合结构。它通过哈希函数将键映射到数组索引,处理哈希冲突时,先用链表串联,当链表长度达到一定阈值后,会自动转换为红黑树以优化查找性能。

深入探讨HashMap的底层,我们得从它的骨架——一个

Node[] table
说起。这本质上就是一个数组,每个数组元素,我们称之为“桶”(bucket),可以存放一个
Node
对象。这个
Node
,其实就是
Entry
的升级版,它存储着键值对(key-value pair)、哈希值以及指向下一个Node的引用(
next
),这正是链表结构的基础。

当你调用

put(key, value)
时,HashMap首先会计算
key
的哈希值。这个哈希值的计算并非简单地直接使用
key.hashCode()
,而是经过一番位运算的“扰动处理”,目的是让哈希值的高位也能参与到最终的索引计算中,从而减少哈希冲突。具体来说,它会把
key.hashCode()
key.hashCode() >>> 16
进行异或操作,这在一定程度上打散了哈希值,让它们分布得更均匀。

得到扰动后的哈希值后,HashMap会用这个哈希值与

table.length - 1
进行按位与操作,得到最终的数组索引。这相当于取模运算,但位运算效率更高。

如果计算出的索引位置是空的,那很简单,直接创建一个新的

Node
并放进去。但如果这个位置已经有Node了,也就是发生了哈希冲突,事情就变得有趣了。

在JDK 8之前,这里通常就是简单的链表追加。但在JDK 8中,为了应对极端情况下的链表过长(导致查找效率退化到O(n)),引入了红黑树。当某个桶位的链表长度达到

TREEIFY_THRESHOLD
(默认是8)时,并且
HashMap
的容量
table.length
也达到了
MIN_TREEIFY_CAPACITY
(默认是64),这个链表就会被“树化”成红黑树。红黑树的查找、插入、删除操作的平均时间复杂度都是O(log n),这大大提升了性能。如果
HashMap
容量不足64,即使链表长度达到8,也不会直接树化,而是会先进行扩容(resize),因为扩容可能让元素重新分布,从而缓解冲突。

get(key)
的逻辑也类似,先计算
key
的哈希值和索引,然后去对应的桶位查找。如果桶位是链表,就遍历链表,通过
equals()
方法找到匹配的键;如果是红黑树,就按照红黑树的查找逻辑来。

HashMap为什么选择“数组+链表/红黑树”的混合结构?

这其实是一个经典的性能与空间平衡问题。单纯的数组查找效率高(O(1)),但一旦哈希冲突,处理起来就麻烦;单纯的链表插入删除快,但查找效率低(O(n))。HashMap巧妙地结合了两者:数组提供快速的索引定位,链表/红黑树处理冲突。

早期版本用链表,简单直接,但在哈希函数设计不佳或数据分布极端时,链表可能变得非常长,导致性能急剧下降。这就像你把所有文件都扔进一个文件夹,找起来自然慢。JDK 8引入红黑树,正是为了解决这个痛点。当冲突严重到一定程度,自动升级为红黑树,将查找复杂度从O(n)优化到O(log n),这是个非常聪明的权衡。它不是一开始就用红黑树,因为红黑树的节点比链表节点占用更多内存,且维护成本更高。只有在必要时才进行“树化”,这体现了其设计上的精妙与实用主义。

HashMap的扩容机制是如何工作的?

HashMap的容量不是一成不变的。当

HashMap
中的元素数量(
size
)超过了容量(
capacity
)与负载因子(
loadFactor
)的乘积,也就是
threshold
时,
HashMap
就会进行扩容操作。默认的负载因子是0.75,这意味着当
HashMap
填满其75%的容量时,它就会扩容。

扩容通常会使底层数组的容量翻倍。例如,如果当前容量是16,扩容后就变成32。扩容不仅仅是简单地增加数组大小,更重要的是要进行“再哈希”(rehash)。这意味着所有旧数组中的元素都需要重新计算它们在新数组中的位置,因为数组长度变了,

hash & (newCapacity - 1)
的结果也会改变。

Memo AI
Memo AI

AI音视频转文字及字幕翻译工具

下载

这个过程是比较耗时的,因为它涉及到遍历所有已存在的Node,并重新分配到新的数组中。如果链表被树化了,在扩容时,红黑树也会被拆分或重新构建。JDK 8在扩容时对链表元素的重新定位做了优化:对于旧桶中的每个节点,如果其哈希值与旧容量进行与操作的结果为0,它就留在原位;如果结果不为0,它就会被移动到

原索引 + 旧容量
的位置。这样,一个旧桶中的链表(或红黑树)会被拆分成两个新桶中的链表(或红黑树),避免了重新计算每个元素的完整哈希值和索引,提高了效率。

扩容是保证HashMap性能的关键。它确保了桶的平均长度不会过长,从而维持了O(1)(平均)的查找和插入性能。但频繁的扩容也会带来性能开销,所以初始容量的选择有时也需要考量。

HashMap在多线程环境下为什么不安全,以及有哪些替代方案?

HashMap在多线程环境下是不安全的,这是一个非常重要的点,也是新手常犯的错误。它的不安全体现在几个方面:

  1. 数据丢失或覆盖:
    put
    操作时,如果两个线程同时尝试在同一个桶位插入元素,可能会导致其中一个线程的更新被另一个线程覆盖,或者链表结构被破坏。
  2. 死循环(JDK 7及之前): 在JDK 7及之前的版本中,扩容时,由于链表头插法和多线程并发操作,可能会形成环形链表,导致
    get
    操作时陷入死循环,CPU占用100%。JDK 8通过改用尾插法和更精细的扩容逻辑,虽然避免了死循环,但仍然无法保证数据一致性。
  3. 脏读: 一个线程在读取
    HashMap
    时,另一个线程可能正在修改它,导致读取到不一致的数据状态。

简单来说,

HashMap
的内部状态在并发修改时无法得到正确维护,因为它没有进行任何同步控制。

那么,在多线程环境下,我们应该使用什么呢?

  • Collections.synchronizedMap(Map<K, V> m)
    这是最简单粗暴的方法,它会返回一个线程安全的
    Map
    视图。它通过对所有方法进行
    synchronized
    同步,确保了原子性。但缺点是,它是一个全局锁,所有操作都必须等待,并发性能非常差。在高并发场景下,这几乎不可用。

  • ConcurrentHashMap
    这是Java并发包
    java.util.concurrent
    下提供的、专门为高并发场景设计的哈希表。它是
    HashMap
    的线程安全版本,但其实现远比
    synchronizedMap
    复杂和高效。

    ConcurrentHashMap
    在JDK 7中采用了
    Segment
    分段锁的机制,将整个
    HashMap
    分成多个小的
    Segment
    ,每个
    Segment
    独立加锁,从而允许多个线程同时访问不同的
    Segment
    ,大大提高了并发度。

    而在JDK 8中,

    ConcurrentHashMap
    进一步优化,放弃了
    Segment
    ,转而采用了“
    CAS + synchronized
    ”的策略。它在
    put
    操作时,先通过
    CAS
    (Compare-And-Swap)尝试更新,如果失败(说明有并发),则使用
    synchronized
    锁住当前桶的头节点(或红黑树的根节点)。这样,锁的粒度更细,只锁定发生冲突的桶,而不是整个
    Map
    或一个大的
    Segment
    ,从而实现了更高的并发性能。同时,它也引入了红黑树来处理链表过长的问题,与
    HashMap
    的设计思想保持一致。

所以,如果你的应用场景涉及多线程,并且需要一个高性能的哈希表,那么

ConcurrentHashMap
几乎是唯一的,也是最佳的选择。不要尝试手动给
HashMap
加锁,那往往会引入更复杂的问题,并且性能通常不如
ConcurrentHashMap

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
length函数用法
length函数用法

length函数用于返回指定字符串的字符数或字节数。可以用于计算字符串的长度,以便在查询和处理字符串数据时进行操作和判断。 需要注意的是length函数计算的是字符串的字符数,而不是字节数。对于多字节字符集,一个字符可能由多个字节组成。因此,length函数在计算字符串长度时会将多字节字符作为一个字符来计算。更多关于length函数的用法,大家可以阅读本专题下面的文章。

954

2023.09.19

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

764

2023.08.10

Python 多线程与异步编程实战
Python 多线程与异步编程实战

本专题系统讲解 Python 多线程与异步编程的核心概念与实战技巧,包括 threading 模块基础、线程同步机制、GIL 原理、asyncio 异步任务管理、协程与事件循环、任务调度与异常处理。通过实战示例,帮助学习者掌握 如何构建高性能、多任务并发的 Python 应用。

376

2025.12.24

java多线程相关教程合集
java多线程相关教程合集

本专题整合了java多线程相关教程,阅读专题下面的文章了解更多详细内容。

30

2026.01.21

C++多线程相关合集
C++多线程相关合集

本专题整合了C++多线程相关教程,阅读专题下面的的文章了解更多详细内容。

29

2026.01.21

C# 多线程与异步编程
C# 多线程与异步编程

本专题深入讲解 C# 中多线程与异步编程的核心概念与实战技巧,包括线程池管理、Task 类的使用、async/await 异步编程模式、并发控制与线程同步、死锁与竞态条件的解决方案。通过实际项目,帮助开发者掌握 如何在 C# 中构建高并发、低延迟的异步系统,提升应用性能和响应速度。

103

2026.02.06

golang map内存释放
golang map内存释放

本专题整合了golang map内存相关教程,阅读专题下面的文章了解更多相关内容。

77

2025.09.05

golang map相关教程
golang map相关教程

本专题整合了golang map相关教程,阅读专题下面的文章了解更多详细内容。

40

2025.11.16

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

4

2026.03.10

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Node.js 教程
Node.js 教程

共57课时 | 13.1万人学习

CSS3 教程
CSS3 教程

共18课时 | 6.9万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号