# 问题
28. 高并发场景下,如果要用 HashMap 但又需要保证线程安全,该如何设计?
# 标准答案
在高并发场景下,原生 HashMap
线程不安全,并发 put()
操作可能导致 数据丢失、死循环 等问题。因此,常见的线程安全替代方案包括:
- 使用
ConcurrentHashMap
(推荐):基于 CAS + 分段锁 机制,支持高并发写入,性能优于Collections.synchronizedMap(new HashMap<>())
。 - 使用
Collections.synchronizedMap(new HashMap<>())
:对整个HashMap
加同步锁,适用于低并发场景,但性能较差。 - 使用
ReadWriteLock
自定义封装HashMap
:读写分离,适用于读多写少的场景,但不如ConcurrentHashMap
高效。 - 基于
CopyOnWriteArrayList
或CopyOnWriteMap
(适用于读多写少):写时复制,避免锁竞争,但写入成本高。
综合来看,大多数情况下 ConcurrentHashMap
是最优选择,除非业务场景有特殊需求。
# 答案解析
# 1. HashMap 为什么不能用于高并发场景?
原生 HashMap
并非线程安全,在多线程并发 put()
时,可能出现以下问题:
- 数据丢失:多个线程同时
put()
时,某些值可能被覆盖,导致丢失。 - 死循环(JDK 1.7):高并发下
rehash()
过程中的链表反转可能导致死循环。 - 扩容竞态条件:多个线程同时触发
resize()
可能导致数据不一致。
示例代码(线程不安全的 HashMap
):
Map<Integer, String> map = new HashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executor.execute(() -> map.put((int) (Math.random() * 100), "value"));
}
executor.shutdown();
System.out.println(map.size()); // 可能小于 100,数据丢失
2
3
4
5
6
7
8
9
# 2. 线程安全的 HashMap 替代方案
# 方案 1:使用 ConcurrentHashMap
(推荐)
ConcurrentHashMap
采用 CAS + Synchronized + 细粒度锁分段,在高并发下性能最优。
原理解析:
- JDK 1.7 采用 Segment 分段锁(ReentrantLock 实现)。
- JDK 1.8 采用 CAS + Synchronized,移除 Segment,提升并发能力。
computeIfAbsent()
等方法减少锁竞争,优化写入性能。
示例代码:
Map<Integer, String> map = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executor.execute(() -> map.put((int) (Math.random() * 100), "value"));
}
executor.shutdown();
System.out.println(map.size()); // 线程安全,数据不会丢失
2
3
4
5
6
7
8
9
# 方案 2:使用 Collections.synchronizedMap()
Collections.synchronizedMap(new HashMap<>())
对 HashMap
进行了 全局同步,即 put()
、get()
操作都被 synchronized
修饰,但 锁粒度过大,写性能低。
Map<Integer, String> syncMap = Collections.synchronizedMap(new HashMap<>());
syncMap.put(1, "value");
2
适用于 低并发场景,但高并发下容易导致 锁竞争严重,影响吞吐量。
# 方案 3:使用 ReadWriteLock
保护 HashMap
如果场景是 读多写少,可以使用 ReadWriteLock
来分离读写操作,减少不必要的锁竞争。
示例代码
class ReadWriteLockMap<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void put(K key, V value) {
lock.writeLock().lock();
try {
map.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
public V get(K key) {
lock.readLock().lock();
try {
return map.get(key);
} finally {
lock.readLock().unlock();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
适用于 读多写少的业务场景,如 配置缓存。
# 方案 4:使用 CopyOnWriteMap
(适用于读多写少)
CopyOnWriteArrayList
适用于 读多写少 的场景,每次 put()
时 复制整个数组,写入代价高,但 读性能极高。
Map<Integer, String> map = new ConcurrentHashMap<>();
缺点:写入开销大,不适用于高频写入。
# 3. 选型对比
方案 | 线程安全 | 适用场景 | 优缺点 |
---|---|---|---|
ConcurrentHashMap | ✅ 高 | 高并发读写 | 写性能最优,适用于大规模并发 |
synchronizedMap(HashMap) | ✅ 低 | 低并发 | 锁粒度大,写入慢 |
ReadWriteLock + HashMap | ✅ 中 | 读多写少 | 读性能较高,写入有锁竞争 |
CopyOnWriteArrayList/Map | ✅ 高 | 极端读多写少 | 写性能极差,不适用于频繁写 |
# 4. 为什么 ConcurrentHashMap
是最优解?
基于 CAS(Compare And Swap)优化写操作
- JDK 1.8 采用 CAS + Synchronized,避免全局锁。
putVal()
仅在 hash 冲突时加锁,大幅提高并发能力。
避免扩容锁竞争
- JDK 1.7
ConcurrentHashMap
采用 Segment,扩容时只影响单个 Segment。 - JDK 1.8 采用 分批迁移,减少扩容时的写阻塞。
- JDK 1.7
支持高并发读写
get()
方法是 无锁的(volatile 读取),不会影响并发查询性能。
# 深入追问
🔹 1. 为什么 ConcurrentHashMap 不能存 null
?
put(null, value)
可能引起 CAS 失败,get(null)
可能导致NullPointerException
,因此 JDK 禁止null
作为 key 或 value。
🔹 2. 为什么 ConcurrentHashMap
JDK 1.8 移除了 Segment?
- Segment 本质是 ReentrantLock 的封装,JDK 1.8 直接使用 CAS + Node + Synchronized 进行加锁,减少额外的 Segment 结构,提高并发性能。
🔹 3. 如何基于 ConcurrentHashMap
设计缓存?
- 可以结合
computeIfAbsent()
实现 线程安全的懒加载缓存。
# 相关面试题
- 为什么 HashMap 线程不安全?ConcurrentHashMap 如何优化?
- 为什么 ConcurrentHashMap 不能存
null
? - ConcurrentHashMap vs. Hashtable vs. SynchronizedMap?
- 如何使用 ReadWriteLock 设计高效的缓存?