【Java面试经典】HashMap线程安全问题,是否有替代方案

142 阅读4分钟

HashMap是线程安全的吗?是否有替代方案呢?

一、HashMap 不是线程安全的

(一)原因

  1. 数据不一致性

    • 在多线程环境下,当多个线程同时对 HashMap 进行写操作(如put方法)时,可能会导致数据不一致。例如,两个线程同时执行put操作,并且计算出相同的数组索引(基于哈希值和数组大小计算),它们可能会同时修改该索引位置上的链表或红黑树结构。这可能会导致元素丢失或者错误地覆盖已有元素,使得 HashMap 中的数据状态不符合预期。
  2. 死循环风险(Java 8 之前主要问题)

    • 在 Java 8 之前,HashMap 在处理哈希冲突时采用链表结构。当多个线程并发地对链表进行操作时,可能会导致链表形成环形结构。例如,在resize(扩容)操作中,涉及到重新计算元素在新数组中的位置并迁移元素。如果两个线程同时进行扩容操作,并且在修改链表的指针时出现交叉,就可能使链表形成环形,导致后续的get操作陷入死循环,使程序出现假死状态。

二、替代方案

(一)Hashtable

  1. 原理

    • Hashtable 是线程安全的哈希表实现。它通过在每个方法(如putgetremove等)上使用synchronized关键字来保证在同一时刻只有一个线程能够访问和修改 Hashtable 的内容。这就好比在一个房间(Hashtable)的门口设置了一把锁,每次只有一个人(线程)能够拿着钥匙(获取锁)进入房间进行操作,其他人需要等待。
  2. 与 HashMap 的区别

    • 性能方面:由于 Hashtable 的方法级同步,在高并发场景下,大量线程需要竞争锁,导致性能开销较大。相比之下,HashMap 在单线程环境下性能较好,因为它没有这些同步开销。
    • 键值是否可为空:Hashtable 不允许键和值为null。如果尝试将null作为键或值插入,会抛出NullPointerException。而 HashMap 允许键为null(最多一个),值也可以为null

(二)Collections.synchronizedMap ()

  1. 原理

    • Collections.synchronizedMap()方法返回一个线程安全的 Map。它是基于已有的 Map(可以是 HashMap 等)进行包装。在这个包装类的方法内部,通过使用synchronized块来对底层的 Map 进行同步访问。具体来说,它以传入的 Map 对象作为锁对象,在每个方法(如putget等)被调用时,会先获取这个锁,然后执行底层 Map 的相应操作,操作完成后再释放锁。
  2. 与其他方案的区别

    • 这种方式提供了一种将非线程安全的 Map 转换为线程安全的 Map 的便捷方法。与 Hashtable 不同的是,它可以基于任意的 Map 实现进行包装。性能方面,和 Hashtable 类似,在高并发场景下会因为同步机制产生性能损耗,因为它也是对整个 Map 进行加锁控制并发访问。

(三)ConcurrentHashMap

  1. 原理(Java 8 及之后)

    • ConcurrentHashMap 在内部采用了更加复杂和高效的并发控制机制。它的结构是Node数组 + 链表 / 红黑树。在并发控制方面,它结合使用了CAS(Compare - and - Swap)操作和synchronized关键字。首先,通过计算键的哈希值来确定元素所在的位置(Node或链表 / 红黑树),在插入或修改元素时,会尝试使用CAS操作进行无锁的更新。如果CAS操作失败(可能因为其他线程同时修改了相同位置),则会使用synchronized锁来保证数据的一致性。这种方式将数据的访问和修改粒度细化,允许多个线程同时对不同位置的数据进行操作,提高了并发性能。
  2. 与其他方案的区别

    • 性能优势:在高并发场景下,ConcurrentHashMap 的性能优于 Hashtable 和Collections.synchronizedMap()包装的 Map。因为它不是对整个 Map 进行加锁,而是根据数据的位置进行细粒度的并发控制,减少了线程等待的时间,能够更好地利用多核处理器的优势。
    • 键是否可为空:ConcurrentHashMap 和 Hashtable 一样,不允许键为null,这是为了避免在并发环境下,null键可能导致的歧义(因为无法区分是没有找到该键还是键本身就是null)。而值可以为null