一、HashMap 为什么不是线程安全的?
HashMap 的线程不安全主要体现在多线程并发修改时,可能导致以下问题:
- 数据覆盖:多个线程同时调用
put()可能导致键值对被覆盖(如哈希冲突时链表节点的插入)。 - 死循环(JDK 1.7) :扩容时链表采用头插法,多线程并发
rehash可能导致链表成环,后续get()操作触发死循环(JDK 1.8 改用尾插法修复)。 - 状态不一致:
size、modCount等字段未同步更新,导致遍历时抛出ConcurrentModificationException。
根本原因:HashMap 内部未使用锁或原子操作保证并发安全,多线程操作共享状态时发生竞态条件(Race Condition)。
二、HashMap 如何解决哈希冲突?
HashMap 采用 拉链法(Separate Chaining) 处理冲突,具体分两阶段优化:
-
链表存储:当多个键的哈希值映射到同一桶(Bucket)时,以链表形式存储键值对。
-
红黑树优化(JDK 1.8+) :
- 当链表长度超过阈值(默认 8),链表转为红黑树,将查询复杂度从
O(n)降至O(log n)。 - 当红黑树节点数小于阈值(默认 6),退化为链表,避免小数据时红黑树维护开销。
- 当链表长度超过阈值(默认 8),链表转为红黑树,将查询复杂度从
三、HashMap 何时扩容?
HashMap 扩容由 负载因子(Load Factor,默认 0.75) 和当前容量决定,触发条件如下:
-
容量阈值:当元素数量超过
容量 × 负载因子(如默认容量 16 时,阈值为12)。 -
扩容过程:
- 新容量为原容量的 2 倍(保证哈希分布均匀)。
- 重新计算所有键的哈希值,分配到新桶中(JDK 1.8 优化为高位掩码判断,减少
rehash计算)。
负载因子 0.75 的权衡:
- 值过高(如 0.9)会减少扩容次数,但哈希冲突概率增加,性能下降。
- 值过低(如 0.5)会频繁扩容,内存利用率降低。
- 0.75 是时间与空间的折中经验值。
四、总结对比
| 问题 | 关键点 |
|---|---|
| 线程不安全 | 无锁设计导致数据覆盖、死循环(JDK 1.7)、状态不一致。 |
| 哈希冲突解决 | 拉链法 + 红黑树优化(JDK 1.8),平衡查询效率与维护成本。 |
| 扩容机制 | 负载因子 0.75 触发 2 倍扩容,rehash 优化减少性能损耗。 |
五、建议
- 线程安全场景:使用
ConcurrentHashMap或Collections.synchronizedMap。 - 哈希冲突高频场景:优化
hashCode()方法,降低冲突概率。 - Android 开发:小数据量时优先使用
ArrayMap或SparseArray(避免自动装箱和内存冗余)。