hashmap

qinyelin
发布于 2026-08-25 / 7 阅读
0
0

hashmap

HashMap 为什么线程不安全?

面试频率:★★★★★

工作频率:★★★★★


⚡30 秒速记(复习只看这里)

🟢 一句话

HashMap 线程不安全,因为多个线程同时修改数组、链表、红黑树时,没有同步控制,会导致数据覆盖、数据丢失。


🟡 HashMap 底层结构

JDK8:

数组(table)

    ↓

链表

    ↓

红黑树(链表长度 >= 8 且数组长度 >= 64)

🟠 三个核心概念

1. Hash 决定桶位置

hash(key)

↓

计算 index

↓

决定放数组哪个位置

2. 链表解决 Hash 冲突

多个 key:

hash 一样

↓

进入同一个桶

↓

形成链表

例如:

1号桶

↓

张三

↓

李四

↓

王五

3. 扩容会重新计算位置

扩容:

数组长度变大

↓

重新计算 index

↓

重新搬迁元素

hash 不变。

但是数组位置可能变化。


📚 完整知识

一、HashMap 底层结构

HashMap 本质:

数组 + 链表 + 红黑树

结构:

table

0

1  ──> Node
          |
          ↓
        Node

2

3  ──> Node

数组中的每一个位置叫:

桶(bucket)


二、put("张三") 发生了什么?

执行:

map.put("张三", 1);

流程:

key

↓

hash()

↓

计算数组下标 index

↓

找到桶

↓

没有元素

↓

直接放入

例如:

数组

0

1  ──> 张三

2

3

三、为什么需要链表?

因为可能出现 Hash 冲突。

例如:

张三.hash()

↓

1号桶


李四.hash()

↓

也是1号桶

如果直接覆盖:

张三 ❌
李四

数据丢失。

所以:

使用链表。

变成:

1号桶

↓

张三

↓

李四

四、扩容(resize)是什么?

当 HashMap 元素太多:

数组容量不够。

例如:

扩容前:

数组长度 = 4


0

1 ──> 张三
        |
        ↓
      李四

2

3 ──> 王五

HashMap 创建更大的数组:

数组长度 = 8


0

1

2

3

4

5

6

7

然后:

把旧数组里的元素重新计算位置。

这个过程:

叫搬迁。


五、扩容后 hash 会变化吗?

不会。

例如:

张三.hashCode()

扩容前:

123456


扩容后:

123456

hash 不变。

变化的是:

数组下标。


例如:

扩容前:

index = hash % 4

123456 % 4

↓

1号桶

扩容后:

index = hash % 8

123456 % 8

↓

5号桶

所以:

hash 一样,但是位置可能不同。


六、头插法是什么?(JDK1.7)

注意:

头插法针对的是:

同一个桶里面的链表。

不是整个 HashMap。

例如:

原链表:

1号桶

张三

↓

李四

↓

王五

搬迁时:

先搬张三:

张三

再搬李四:

放链表头:

李四

↓

张三

再搬王五:

继续放头:

王五

↓

李四

↓

张三

特点:

新元素插入链表头部。


七、尾插法是什么?(JDK1.8)

JDK8 改成尾插法。

例如:

原:

张三

↓

李四

↓

王五

搬迁:

保持顺序:

张三

↓

李四

↓

王五

不会反转。


八、为什么 JDK1.7 扩容会死循环?

原因:

多线程同时扩容。

JDK1.7:

使用头插法。

两个线程:

同时修改链表 next 指针。

可能形成:

张三

↓

李四

↓

王五

↑      |
|______|

形成环。

之后:

执行:

map.get(key)

遍历链表:

张三

↓

李四

↓

王五

↓

张三

↓

李四

...

永远结束不了。

导致:

CPU 100%。


九、为什么 JDK1.8 还线程不安全?

虽然:

JDK8 修复了死循环。

但是:

仍然存在:

1. 数据覆盖

两个线程同时 put:

线程A 写入

↓

线程B覆盖

↓

A的数据丢失

2. 数据丢失

扩容过程中:

多个线程同时修改数组。

可能导致:

部分数据没有迁移成功。


所以:

HashMap:

❌ 多线程环境不能直接使用。


十、ConcurrentHashMap 为什么安全?

ConcurrentHashMap:

JDK8:

主要使用:

CAS

+

synchronized

保证并发修改安全。

相比:

Collections.synchronizedMap()

性能更好。


💼 工作场景

错误:

Map<String,Object> cache =
        new HashMap<>();

多个线程:

put()

get()

高并发情况下:

可能:

数据异常

偶发Bug

数据丢失

正确:

单线程:

HashMap

多线程:

ConcurrentHashMap

🎤 面试回答(30秒)

HashMap 是线程不安全的,因为它内部使用数组加链表或者红黑树结构存储数据,在多线程环境下多个线程同时执行 put、resize 等操作时,没有同步机制保护,可能导致数据覆盖和数据丢失。JDK1.7 扩容时采用头插法,多线程情况下可能形成环形链表导致死循环。JDK1.8 改成尾插法解决了死循环问题,但是 HashMap 仍然不是线程安全的,所以并发场景一般使用 ConcurrentHashMap。


🎯 面试追问

Q1:Hash 决定什么?

答:

决定数组桶的位置。

不是决定链表顺序。


Q2:链表什么时候出现?

答:

多个 key 经过 hash 后进入同一个数组位置,也就是发生 Hash 冲突。


Q3:扩容后两个元素还在一起吗?

答:

不一定。

hash 不变。

但是数组长度变化,index 重新计算。

可能分开。


Q4:头插法插在哪里?

答:

插入同一个桶里的链表头部。

不是数组头。


Q5:为什么 JDK8 不会死循环?

答:

因为改成尾插法,避免扩容时链表顺序反转导致环形成。


⚠️ 工作踩坑

✅ 多线程缓存使用 HashMap

问题:

数据异常。


✅ 认为 JDK8 HashMap 线程安全

错误。

JDK8 只是解决扩容死循环。


✅ 不了解 Hash 冲突

误以为:

一个 hash 对应一个元素。

实际上:

多个元素可能进入同一个桶。


❓ 自测

  1. HashMap 底层结构是什么?

  2. 为什么需要链表?

  3. hash 一样,扩容后位置一定一样吗?

  4. 什么是搬迁?

  5. 头插法插的是哪里?

  6. JDK1.7 为什么会死循环?

  7. JDK1.8 为什么仍然线程不安全?

  8. 多线程环境为什么用 ConcurrentHashMap?


评论