详解:HashMap为什么线程不安全、如何解决散列碰撞以及何时扩容。

422 阅读2分钟

一、HashMap 为什么不是线程安全的?

HashMap 的线程不安全主要体现在多线程并发修改时,可能导致以下问题:

  1. 数据覆盖:多个线程同时调用 put() 可能导致键值对被覆盖(如哈希冲突时链表节点的插入)。
  2. 死循环(JDK 1.7) :扩容时链表采用头插法,多线程并发 rehash 可能导致链表成环,后续 get() 操作触发死循环(JDK 1.8 改用尾插法修复)。
  3. 状态不一致sizemodCount 等字段未同步更新,导致遍历时抛出 ConcurrentModificationException

根本原因HashMap 内部未使用锁或原子操作保证并发安全,多线程操作共享状态时发生竞态条件(Race Condition)。

二、HashMap 如何解决哈希冲突?

HashMap 采用 拉链法(Separate Chaining)  处理冲突,具体分两阶段优化:

  1. 链表存储:当多个键的哈希值映射到同一桶(Bucket)时,以链表形式存储键值对。

  2. 红黑树优化(JDK 1.8+)

    • 当链表长度超过阈值(默认 8),链表转为红黑树,将查询复杂度从 O(n) 降至 O(log n)
    • 当红黑树节点数小于阈值(默认 6),退化为链表,避免小数据时红黑树维护开销。

三、HashMap 何时扩容?

HashMap 扩容由 负载因子(Load Factor,默认 0.75)  和当前容量决定,触发条件如下:

  1. 容量阈值:当元素数量超过 容量 × 负载因子(如默认容量 16 时,阈值为 12)。

  2. 扩容过程

    • 新容量为原容量的 2 倍(保证哈希分布均匀)。
    • 重新计算所有键的哈希值,分配到新桶中(JDK 1.8 优化为高位掩码判断,减少 rehash 计算)。

负载因子 0.75 的权衡

  • 值过高(如 0.9)会减少扩容次数,但哈希冲突概率增加,性能下降。
  • 值过低(如 0.5)会频繁扩容,内存利用率降低。
  • 0.75 是时间与空间的折中经验值。

四、总结对比

问题关键点
线程不安全无锁设计导致数据覆盖、死循环(JDK 1.7)、状态不一致。
哈希冲突解决拉链法 + 红黑树优化(JDK 1.8),平衡查询效率与维护成本。
扩容机制负载因子 0.75 触发 2 倍扩容,rehash 优化减少性能损耗。

五、建议

  1. 线程安全场景:使用 ConcurrentHashMap 或 Collections.synchronizedMap
  2. 哈希冲突高频场景:优化 hashCode() 方法,降低冲突概率。
  3. Android 开发:小数据量时优先使用 ArrayMap 或 SparseArray(避免自动装箱和内存冗余)。

更多分享

  1. 一文带你吃透Android中常见的高效数据结构
  2. 详解:ArrayMap和SparseArray在HashMap上面的改进
  3. 详解:HashMap与TreeMap、HashTable的区别
  4. 详解:Set集合是如何保证元素不重复的
  5. 详解:LinkedHashMap的工作原理和实现
  6. 一文带你搞懂HashSet和TreeSet的区别