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,另一线程修改结构(如
put或remove)。 -
原因:
- 迭代器通过
modCount检测并发修改,但该机制并非线程安全。 - 多线程下
modCount可能被覆盖,导致迭代器无法正确抛出ConcurrentModificationException。
- 迭代器通过
-
源码示例:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
总结
| 问题类型 | JDK版本 | 具体表现 | 根本原因 |
|---|---|---|---|
| 数据覆盖 | 所有版本 | 键值对丢失 | 非原子操作(如桶初始化) |
| 链表成环 | JDK 1.7 | get操作死循环 | 头插法扩容并发冲突 |
| 扩容数据丢失 | 所有版本 | 部分数据未被迁移到新数组 | 多线程覆盖新数组 |
| 红黑树结构破坏 | JDK 1.8+ | 树操作异常(如查询结果错误) | 并发修改树节点 |
| size不一致 | 所有版本 | size与实际元素数不符 | 非原子更新 |
| 迭代器失效 | 所有版本 | 未正确抛出并发修改异常 | modCount非线程安全 |
解决方案
-
使用线程安全的替代类:
ConcurrentHashMap(推荐,高并发优化)。Collections.synchronizedMap(new HashMap<>())(性能较低)。
-
避免多线程共享HashMap: 每个线程使用独立的HashMap实例。
-
同步控制: 手动加锁(如
synchronized),但会牺牲性能。