hashmap线程不安全原因

138 阅读3分钟

HashMap线程不安全的原因详解如下:


1. 数据覆盖问题

  • 场景:多个线程同时执行put操作,且键的哈希值相同(同一桶)。

  • 原因

    • 线程A和线程B同时检查桶为空,均创建新节点并尝试插入。
    • 若未同步,最终只有一个节点的插入生效,另一线程的数据被覆盖。
  • 源码示例

    // HashMap的putVal方法(简化)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null); // 多线程可能同时进入此分支
    

2. 链表成环(JDK 1.7 特有)

  • 场景:并发扩容时,多线程操作链表迁移。

  • 原因

    • JDK 1.7使用头插法迁移链表,导致并发操作可能形成环形链表。
    • 后续的get操作遍历链表时,触发死循环。
  • 源码示例(JDK 1.7的transfer方法):

    while (e != null) {
        Entry<K,V> next = e.next; // 线程A执行至此挂起
        e.next = newTable[i];     // 线程B完成迁移后,链表成环
        newTable[i] = e;
        e = next;
    }
    

3. 扩容导致的数据丢失

  • 场景:多线程同时触发扩容(resize)。

  • 原因

    • 线程A和线程B各自创建新数组并迁移数据。
    • 最终只有最后一个完成迁移的线程的新数组生效,其他线程迁移的数据丢失。
  • 源码示例(JDK 1.8的resize方法):

    Node<K,V>[] newTab = new Node[newCap];
    table = newTab; // 多线程可能覆盖彼此的新数组
    

4. 红黑树结构破坏(JDK 1.8+)

  • 场景:并发操作导致红黑树节点分裂或合并异常。

  • 原因

    • 当链表转换为红黑树时(树化),多线程可能同时修改树结构。
    • 例如,线程A正在树化,线程B插入节点,导致树平衡破坏。
  • 源码示例(JDK 1.8的treeifyBin方法):

    synchronized (f) { // 仅锁住桶头节点,但并发仍可能破坏树结构
        // 树化逻辑
    }
    

5. size计数器不一致

  • 场景:多线程同时修改size字段。

  • 原因

    • size字段的更新是非原子操作(如size++)。
    • 线程A和线程B同时读取并修改size,导致最终值小于实际插入数量。
  • 源码示例

    // HashMap的addCount方法(JDK 1.8)
    if ((s = size) >= threshold)
        resize();
    size = s + 1; // 非原子操作
    

6. 迭代器的快速失败(Fail-Fast)机制失效

  • 场景:一个线程遍历HashMap,另一线程修改结构(如putremove)。

  • 原因

    • 迭代器通过modCount检测并发修改,但该机制并非线程安全。
    • 多线程下modCount可能被覆盖,导致迭代器无法正确抛出ConcurrentModificationException
  • 源码示例

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
    

总结

问题类型JDK版本具体表现根本原因
数据覆盖所有版本键值对丢失非原子操作(如桶初始化)
链表成环JDK 1.7get操作死循环头插法扩容并发冲突
扩容数据丢失所有版本部分数据未被迁移到新数组多线程覆盖新数组
红黑树结构破坏JDK 1.8+树操作异常(如查询结果错误)并发修改树节点
size不一致所有版本size与实际元素数不符非原子更新
迭代器失效所有版本未正确抛出并发修改异常modCount非线程安全

解决方案

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

    • ConcurrentHashMap(推荐,高并发优化)。
    • Collections.synchronizedMap(new HashMap<>())(性能较低)。
  2. 避免多线程共享HashMap: 每个线程使用独立的HashMap实例。

  3. 同步控制: 手动加锁(如synchronized),但会牺牲性能。