Stay Hungry,Stay Foolish!

为什么 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),让多个线程同时读写天气数据而不出错,极大地减少了去数据库查询的次数。

总结一张图

特性HashMapConcurrentHashMap
形象比喻 无人的公共储物柜 带独立锁的高级储物柜
多线程写入 ❌ 数据会丢、会乱 ✅ 安全,各存各的
性能 单线程极快 多线程下依然很快
适用场景 局部变量、单线程环境 Web 服务、高并发缓存

一句话建议: 只要你的代码运行在多线程环境(比如 Spring Boot 的 Controller 或 Service 层),请无脑选择 ConcurrentHashMap,远离 HashMap 的坑!


这篇博客可以直接发,需要我再帮你配几张更直观的示意图吗?

posted @ 2026-09-30 09:51  lightsong  阅读(5)  评论(0)    收藏  举报
千山鸟飞绝,万径人踪灭