# 问题

28. 高并发场景下,如果要用 HashMap 但又需要保证线程安全,该如何设计?

# 标准答案

在高并发场景下,原生 HashMap 线程不安全,并发 put() 操作可能导致 数据丢失、死循环 等问题。因此,常见的线程安全替代方案包括:

  1. 使用 ConcurrentHashMap(推荐):基于 CAS + 分段锁 机制,支持高并发写入,性能优于 Collections.synchronizedMap(new HashMap<>())
  2. 使用 Collections.synchronizedMap(new HashMap<>()):对整个 HashMap 加同步锁,适用于低并发场景,但性能较差。
  3. 使用 ReadWriteLock 自定义封装 HashMap:读写分离,适用于读多写少的场景,但不如 ConcurrentHashMap 高效。
  4. 基于 CopyOnWriteArrayListCopyOnWriteMap(适用于读多写少):写时复制,避免锁竞争,但写入成本高。

综合来看,大多数情况下 ConcurrentHashMap 是最优选择,除非业务场景有特殊需求。

# 答案解析

# 1. HashMap 为什么不能用于高并发场景?

原生 HashMap 并非线程安全,在多线程并发 put() 时,可能出现以下问题:

  1. 数据丢失:多个线程同时 put() 时,某些值可能被覆盖,导致丢失。
  2. 死循环(JDK 1.7):高并发下 rehash() 过程中的链表反转可能导致死循环
  3. 扩容竞态条件:多个线程同时触发 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,数据丢失
1
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()); // 线程安全,数据不会丢失
1
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");
1
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();
        }
    }
}
1
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<>();
1

缺点:写入开销大,不适用于高频写入。

# 3. 选型对比

方案 线程安全 适用场景 优缺点
ConcurrentHashMap ✅ 高 高并发读写 写性能最优,适用于大规模并发
synchronizedMap(HashMap) ✅ 低 低并发 锁粒度大,写入慢
ReadWriteLock + HashMap ✅ 中 读多写少 读性能较高,写入有锁竞争
CopyOnWriteArrayList/Map ✅ 高 极端读多写少 写性能极差,不适用于频繁写

# 4. 为什么 ConcurrentHashMap 是最优解?

  1. 基于 CAS(Compare And Swap)优化写操作

    • JDK 1.8 采用 CAS + Synchronized,避免全局锁。
    • putVal() 仅在 hash 冲突时加锁,大幅提高并发能力。
  2. 避免扩容锁竞争

    • JDK 1.7 ConcurrentHashMap 采用 Segment,扩容时只影响单个 Segment。
    • JDK 1.8 采用 分批迁移,减少扩容时的写阻塞。
  3. 支持高并发读写

    • 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 设计高效的缓存?