为什么 HashMap 不是线程安全的?(小白也能看懂的图解版)
这是一篇为您准备的技术博客草稿。它结合了您截图中的 Spring Boot 缓存项目背景,用通俗易懂的“小白”视角解释了为什么 HashMap 不是线程安全的,并自然引出了项目中使用的 ConcurrentHashMap。
您可以直接复制以下内容发布到博客平台(如 CSDN、掘金、知乎或个人博客)。
为什么 HashMap 不是线程安全的?(小白也能看懂的图解版)
在最近的 Spring Boot 开发中,我接触到了一个天气查询项目(Weather App with Spring Boot Caching)。在这个项目中,为了优化性能,开发者使用了 ConcurrentHashMap 来实现内存缓存,而不是我们熟悉的 HashMap。
很多初学者会有疑问:明明 HashMap 很好用,为什么要换成 ConcurrentHashMap?HashMap 到底哪里不安全?
今天,我们就用最通俗的语言,把这个问题讲清楚。
1. 什么是 HashMap?把它想象成“储物柜”
为了理解原理,我们把 HashMap 想象成银行里的一排公共储物柜。
- Key(键):就是柜子的编号(比如 101 号柜)。
- Value(值):就是你存在里面的包。
- Thread(线程):就是来存取东西的顾客。
在单线程环境下,只有你一个人用这排柜子,你想怎么存就怎么存,非常安全,速度也最快。
但在多线程环境下(比如 Web 服务器),就像有一群顾客同时冲向这排柜子,如果没有管理员, chaos(混乱)就发生了。
2. HashMap 的“三大罪状”
罪状一:数据覆盖(写丢失)—— “我的苹果去哪了?”
这是最常见的问题。
场景模拟: 假设 1 号柜子是空的。
- 顾客 A(线程 A) 走过来,看了一眼:“1 号柜空的,我要放苹果。”
- 顾客 B(线程 B) 同时也走过来,他也看了一眼:“1 号柜也是空的(因为 A 还没来得及关门),我要放香蕉。”
结果: A 把苹果放进去了,紧接着 B 把香蕉放进去,直接把 A 的苹果挤掉了。 最后柜子里只有香蕉,A 的数据凭空消失了。这就是所谓的非原子性操作导致的数据丢失。
罪状二:死循环(JDK 1.7 的噩梦)—— “鬼打墙”
虽然 JDK 1.8 已经修复了这个问题,但它是 HashMap 历史上最著名的 Bug。
在旧版本中,当柜子满了需要扩容(搬家到更大的柜子)时,HashMap 会把旧链表里的节点迁移到新链表。
场景模拟:
- 顾客 A 正在搬运两个连在一起的箱子(节点 A -> 节点 B)。
- 顾客 B 手快,也在搬,结果把顺序搞反了(变成了 B -> A)。
- 顾客 A 没反应过来,继续按原来的逻辑搬,结果把 A 的尾巴连到了 B,B 的尾巴又连回了 A。
这就形成了一个死循环链表(A 指向 B,B 指向 A)。 以后只要有顾客想查这个格子里的东西,程序就会在这个圈里无限转圈,导致 CPU 占用率飙升到 100%,服务器直接卡死。
(注:JDK 1.8 改用了“尾插法”避免了这个问题,但依然存在数据覆盖问题)
罪状三:读脏数据 —— “看花眼了”
场景模拟: 顾客 A 正在对柜子进行大装修(比如扩容,或者把链表转换成红黑树)。此时柜子处于一种“拆了一半”的中间状态。 顾客 B 此时跑过来读取数据。
结果: B 看到的是一个乱七八糟的结构,甚至可能读到空指针(NullPointer),导致程序直接崩溃报错。
3. 解决方案:请个“智能保安”
既然 HashMap 是个“没人管的柜子”,那解决办法就是给它加锁。在 Java 中,我们通常有两种选择:
方案 A:Collections.synchronizedMap(笨办法)
这就像给整个储物间装了一扇大铁门。不管谁要用,必须先把大门锁上,用完再开。
- 缺点:太慢了!一次只能进一个人,其他人都在门口排队。
方案 B:ConcurrentHashMap(聪明办法)✅
这就是我在 Spring Boot 项目中推荐使用的方案。 它就像给储物柜的每一个小格子(或分段)都配了一把独立的锁。
- 顾客 A 在操作 1 号柜,锁住 1 号柜。
- 顾客 B 想操作 2 号柜?没问题,直接进,互不干扰!
既保证了安全,又保证了极高的并发性能。
4. 结合实战:Spring Boot 中的缓存
回到我最近看的这个 Weather App 项目。
项目介绍里写道:
This project demonstrates the implementation of caching mechanisms... using both in-memory caching (ConcurrentHashMap) and Redis.
在这个天气应用中,可能会有成千上万个用户同时请求“北京今天的天气”。
- 如果用 HashMap:一旦并发请求上来,缓存数据可能会乱套,甚至导致服务崩溃。
- 使用 ConcurrentHashMap:它可以安全地作为本地缓存(Local Cache),让多个线程同时读写天气数据而不出错,极大地减少了去数据库查询的次数。
总结一张图
| 特性 | HashMap | ConcurrentHashMap |
|---|---|---|
| 形象比喻 | 无人的公共储物柜 | 带独立锁的高级储物柜 |
| 多线程写入 | ❌ 数据会丢、会乱 | ✅ 安全,各存各的 |
| 性能 | 单线程极快 | 多线程下依然很快 |
| 适用场景 | 局部变量、单线程环境 | Web 服务、高并发缓存 |
一句话建议: 只要你的代码运行在多线程环境(比如 Spring Boot 的 Controller 或 Service 层),请无脑选择 ConcurrentHashMap,远离 HashMap 的坑!
这篇博客可以直接发,需要我再帮你配几张更直观的示意图吗?

浙公网安备 33010602011771号