ConcurrentHashMap

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

ConcurrentHashMap

ConcurrentHashMap 为什么线程安全?

面试频率:★★★★★
工作频率:★★★★★


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

🟢 一句话

ConcurrentHashMap 通过 CAS + synchronized 保证线程安全,并且只锁当前桶(数组下标对应的位置),避免锁整个 Map,提高并发性能。


🟡 JDK7 和 JDK8 区别

JDK7

使用:

Segment 分段锁

结构:

ConcurrentHashMap

Segment0 🔒

Segment1 🔒

Segment2 🔒

锁的是:

一段数据


JDK8

取消 Segment。

结构:

table

0

1

2

3

5 🔒

6

锁的是:

某一个桶(bin)

也就是:

数组某个下标对应的链表或红黑树。


🟠 JDK8 加锁流程

情况1:桶为空

table[5] == null

↓

CAS 插入

↓

成功

不需要加锁。


情况2:桶已有数据

table[5]

↓

Node

↓

Node

修改链表/红黑树:

synchronized(桶头节点)

锁当前桶。


🔴 一句话

JDK8 ConcurrentHashMap 不是先 synchronized 再 CAS,而是根据场景选择:空桶使用 CAS,有冲突修改桶结构时使用 synchronized。


📚 完整知识

一、为什么需要 ConcurrentHashMap?

HashMap:

线程A

put()


线程B

put()

同时修改:

数组

链表

红黑树

可能导致:

  • 数据覆盖
  • 数据丢失
  • JDK1.7 扩容死循环

所以:

HashMap 不适合多线程环境。


二、为什么不用 synchronized 给整个 Map 加锁?

例如:

synchronized(map){

    map.put("A",1);

}

虽然安全。

但是:

锁太大。

例如:

线程A 修改 table[5]

线程B 修改 table[20]

两个操作没有关系。

但是整张 Map 被锁住。

并发性能下降。


三、ConcurrentHashMap 核心思想

一句话:

缩小锁的范围。

不是:

整个 Map 一把锁

而是:

一个桶一把锁

例如:

table

0

1

2 🔒

3

4

5 🔒

线程A:

修改:

table[2]

线程B:

修改:

table[5]

可以同时执行。


四、JDK7 分段锁

JDK7:

结构:

ConcurrentHashMap


-----------------
|       |       |
Seg0   Seg1    Seg2

 🔒     🔒      🔒

每个 Segment:

拥有自己的锁。

例如:

线程A:

修改 Segment0

线程B:

修改 Segment1

可以同时执行。


缺点:

锁粒度还是比较大。


五、JDK8 实现原理

JDK8:

取消 Segment。

结构类似 HashMap:

table

0

1

2

3

4

5

采用:

CAS

+

synchronized

六、为什么桶为空使用 CAS?

假设:

table[5] == null

两个线程:

同时 put:

线程A

发现为空


线程B

发现为空

如果都直接写:

可能冲突。

所以使用 CAS:

比较:

现在是不是还是 null?

如果是:

替换成 Node

如果不是:

失败重试

结果:

线程A CAS成功

线程B CAS失败

七、为什么桶有数据使用 synchronized?

例如:

table[5]

↓

A

↓

B

现在插入:

C

需要修改:

A.next

B.next

链表结构

甚至可能:

红黑树调整

涉及多个状态。

CAS 不适合。

所以:

synchronized (头节点){

}

锁住当前桶。


八、为什么不是先 CAS 再 synchronized?

因为:

两者解决的问题不同。


CAS

适合:

单个变量修改。

例如:

null → Node

synchronized

适合:

复杂结构修改。

例如:

链表插入

红黑树调整

所以:

CAS 和 synchronized 没有固定先后,根据场景选择。


九、ConcurrentHashMap 锁在哪里?

JDK8:

锁的是:

table[index]

对应的桶。

例如:

table[5]

↓

NodeA

↓

NodeB

锁:

NodeA(头节点)

不是:

❌ 整个 Map

❌ 一段数组

❌ Segment


十、为什么 CAS 不能修改链表?

CAS 适合:

单个变量更新。

例如:

count:

0 → 1

但是链表修改:

需要改变:

Node.next

尾节点

链表结构

多个状态。

所以:

使用 synchronized。


💼 工作场景

SpringBoot 全局缓存

错误:

@Service
public class UserService {

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

}

多个 HTTP 请求:

请求1 → 线程A

请求2 → 线程B

同时修改:

HashMap

可能出现线程安全问题。


正确:

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

🎤 面试回答(30秒)

ConcurrentHashMap 是为了解决 HashMap 在并发环境下线程不安全的问题。JDK7 使用 Segment 分段锁,把数据拆分成多个段,每个段独立加锁。JDK8 取消 Segment,采用 CAS 和 synchronized 实现并发控制。对于空桶插入使用 CAS,提高并发性能;对于已经存在节点的桶,因为需要修改链表或红黑树结构,所以使用 synchronized 锁住当前桶。相比 Hashtable 锁整个 Map,ConcurrentHashMap 锁粒度更小,并发性能更好。


🎯 面试追问

Q1:JDK8 锁的是整个 Map 吗?

答:

不是。

锁的是:

table[index]

对应的桶。


Q2:两个线程同时操作 table[5] 怎么办?

答:

竞争同一个桶。

一个线程获取 synchronized 锁。

另一个等待。


Q3:为什么不用 synchronized 给整个 Map 加锁?

答:

锁粒度太大。

不同桶之间没有竞争。

降低并发性能。


Q4:CAS 和 synchronized 谁优先?

答:

没有固定优先级。

桶为空使用 CAS。

桶已有数据修改结构使用 synchronized。


Q5:ConcurrentHashMap 可以完全替代 HashMap 吗?

答:

不能。

单线程场景 HashMap 性能更好。

只有并发修改场景才使用 ConcurrentHashMap。


⚠️ 工作踩坑

1. 多线程环境使用 HashMap

问题:

数据异常

数据丢失

2. 认为 JDK8 HashMap 线程安全

错误。

JDK8 只是解决扩容死循环。


3. 认为 ConcurrentHashMap 所有操作都是原子

错误:

if(!map.containsKey(key)){
    map.put(key,value);
}

多个线程仍可能重复执行。

应该:

map.putIfAbsent(key,value);

❓ 自测

  1. ConcurrentHashMap 为什么线程安全?

因为 JDK8 使用 CAS + synchronized。
空桶插入使用 CAS,同一个桶发生竞争时使用 synchronized 锁住当前桶。

  1. JDK7 和 JDK8 区别?

JDK7 使用 Segment 分段锁,锁的是一段数据。
JDK8 去掉 Segment,使用 CAS + synchronized,锁粒度降低到桶级别。

  1. JDK8 锁哪里?

锁的是数组某个下标对应的桶,也就是链表头节点或者红黑树节点。

  1. 为什么桶为空使用 CAS?

多个线程可能同时初始化空桶,CAS保证只有一个线程成功。

  1. 为什么桶有数据使用 synchronized?

因为修改链表或红黑树涉及多个节点变化,CAS不适合。

  1. 为什么CAS不适合链表?

CAS适合单个变量修改,链表需要修改多个引用关系。

  1. Hashtable为什么慢?

Hashtable方法级 synchronized,相当于整个Map一把锁。

  1. SpringBoot全局Map为什么用ConcurrentHashMap?

Spring Bean默认单例,多请求共享同一个Map,需要保证线程安全。

Spring Bean 单例和线程安全

Spring Bean 默认单例。

单例本身不是问题。

问题是:

单例 Bean 中存在共享可变成员变量。

例如:

private HashMap cache;

多个 HTTP 请求线程同时访问。

需要:

ConcurrentHashMap / 加锁 / 外部缓存(Redis)


评论