Skip to content

hashmap 多线程情况下使用会出现什么问题

约 1036 字大约 3 分钟

多线程与并发美团

2025-03-26

⭐ 题目日期:

美团 - 2024/12/23

📝 题解:

多线程环境下直接使用 HashMap 会导致一系列问题,主要原因是 HashMap 非线程安全,其内部实现未做同步控制。以下是具体问题和原因分析:


1. 数据不一致(脏读、丢失更新)

  • 问题:多个线程同时执行 put()get() 时,可能导致读取到过期的数据或更新丢失。
  • 原因
    • put() 操作非原子性,涉及哈希计算、链表/红黑树插入等步骤,线程A的修改可能被线程B覆盖。
    • get() 可能读到其他线程未完成修改的中间状态(如扩容时的临时不一致状态)。

示例

// 线程A和线程B同时执行 put("key", 1) 和 put("key", 2)
// 最终结果可能是1或2,而非预期的递增操作。

2. 死循环(JDK 1.7及之前版本)

  • 问题:多线程并发扩容时可能导致CPU占用100%(死循环)。
  • 原因
    • JDK 1.7的 HashMap 使用头插法转移链表节点,并发扩容时可能形成环形链表(线程A和线程B互相指向对方的节点)。
    • 后续调用 get()put() 遍历链表时陷入无限循环。

注意:JDK 1.8改为尾插法,解决了死循环问题,但仍存在数据不一致风险。


3. 扩容时节点丢失

  • 问题:多线程同时触发扩容(resize())可能导致部分键值对丢失。
  • 原因
    • 扩容时需要重新计算桶位置并迁移节点,多个线程可能覆盖彼此的迁移结果。

示例

// 线程A迁移了节点1,线程B迁移了节点2,但最终只有其中一个线程的迁移结果生效。

4. 并发修改异常(ConcurrentModificationException

  • 问题:一个线程遍历 HashMap(如 iterator()),另一个线程修改结构(put()/remove()),会抛出此异常。
  • 原因
    • HashMap 的迭代器通过 modCount 字段检测结构性修改,多线程操作导致该字段不一致。

多线程下 HashMap 的问题总结

问题原因JDK版本影响
数据不一致(脏读/丢失)put()/get() 非原子性,无同步控制所有版本
死循环JDK 1.7 头插法扩容形成环形链表仅JDK 1.7及之前
节点丢失多线程扩容时迁移覆盖所有版本
并发修改异常迭代器遍历时检测到其他线程的结构修改(modCount变化)所有版本

解决方案

1. 使用线程安全的替代类

  • ConcurrentHashMap

    • JDK提供的线程安全哈希表,采用分段锁(JDK 1.7)或CAS + synchronized(JDK 1.8+),性能高。
    • 推荐场景:高并发读写。
  • Collections.synchronizedMap()

    • 包装 HashMap,所有方法用 synchronized 加锁,但性能较差(全表锁)。
    • 示例
      Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

2. 手动加锁(不推荐)

  • 使用 synchronizedReentrantLock 控制所有 HashMap 操作,但代码复杂且性能低。
    synchronized (map) {
        map.put("key", value);
    }

3. 避免共享状态

  • 每个线程使用独立的 HashMap(如 ThreadLocal 封装),但仅适用于特定场景。

为什么 ConcurrentHashMap 更优?

  • 分段锁(JDK 1.7):将数据分到多个段(Segment),不同段可并发操作。
  • CAS + synchronized(JDK 1.8+):仅锁单个桶(Node),进一步降低锁粒度。
  • 无死循环风险:设计上避免环形链表问题。

代码验证示例

public class HashMapConcurrencyDemo {
    public static void main(String[] args) throws InterruptedException {
        Map<String, Integer> unsafeMap = new HashMap<>();
        // Map<String, Integer> safeMap = new ConcurrentHashMap<>();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                unsafeMap.put(Thread.currentThread().getName() + i, i);
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Map size: " + unsafeMap.size()); 
        // 可能输出小于2000(数据丢失)
    }
}

输出结果
使用 HashMap 时,size() 可能小于预期(如1735),而 ConcurrentHashMap 总能保证正确性。


总结

  • 不要在多线程中直接使用 HashMap,优先选择 ConcurrentHashMap
  • 若必须使用 HashMap,需确保所有操作外部同步(但性能差)。
  • JDK 1.8+的 HashMap 虽解决死循环,仍存在数据竞争问题。