线程安全是指在多线程(并发)环境下,多个线程同时操作同一个对象时,不会出现不符合预期的错误结果。然而 HashMap 却是线程不安全的,也就是当多个线程同时操作 HashMap 时,会出现不确定的错误结果。

为什么线程不安全?

HashMap 非线程安全主要是因为 HashMap 的设计中,未采用任何同步机制(锁机制)来保证其安全性。 这一点在它的源码注释中也有说明,如下图所示:

线程不安全问题展现

HashMap 线程不安全主要体现在以下两方面:

  1. 在 JDK 1.7 中的死循环问题
  2. 所有版本中的数据覆盖问题

死循环问题

死循环问题是指在并发环境下,因为多个线程同时进行 put 操作,导致链表形成环形数据结构,一旦形成环形数据结构,在 get(key) 的时候就会产生死循环。如下图所示:

死循环原因

HashMap 导致死循环的原因是由以下条件共同导致的:

  1. HashMap 使用头插法进行数据插入(JDK 1.8 之前);
  2. 多线程同时添加;
  3. 触发了 HashMap 扩容。

当满足以上所有条件时,HashMap 就会出现死循环问题。

数据覆盖问题

数据覆盖问题发生在并发添加元素的场景下,它不止出现在 JDK 1.7 版本中,其他版本中也存在此问题,数据覆盖产生的流程如下:

  1. 线程 T1 进行添加时,判断某个位置可以插入元素,但还没有真正的进行插入操作,自己时间片就用完了。
  2. 线程 T2 也执行添加操作,并且 T2 产生的哈希值和 T1 相同,也就是 T2 即将要存储的位置和 T1 相同,因为此位置尚未插入值(T1 线程执行了一半),于是 T2 就把自己的值存入到当前位置了。
  3. T1 恢复执行之后,因为非空判断已经执行完了,它感知不到此位置已经有值了,于是就把自己的值也插入到了此位置,那么 T2 的值就被覆盖了。

具体执行流程如下图所示。

数据覆盖执行步骤一

线程 T1 准备将数据 k1:v1 插入到 Null 处,但还没有真正的执行,自己的时间片就用完了,进入休眠状态了,如下图所示:

数据覆盖执行步骤二

线程 T2 准备将数据 k2:v2 插入到 Null 处,因为此处现在并未有值,如果此处有值的话,它会使用链式法将数据插入到下一个没值的位置上,但判断之后发现此处并未有值,那么就直接进行数据插入了,如下图所示:

数据覆盖执行步骤三

线程 T2 执行完成之后,线程 T1 恢复执行,因为线程 T1 之前已经判断过此位置没值了,所以会直接插入,此时线程 T2 插入的值就被覆盖了,如下图所示:

解决方案

HashMap 非线程安全的解决方案有以下几个:

  1. 使用线程安全容器 ConcurrentHashMap 替代 HashMap;
  2. 使用线程安全容器 Hashtable 替代 HashMap(此方式性能不高,不推荐使用);
  3. 在进行 HashMap 操作时,使用 synchronized 或 Lock 加锁执行。

小结

HashMap 线程不安全的主要原因,是因为 HashMap 的设计中未采用任何同步机制(锁机制)来保证其安全性。它的线程不安全主要表现在死循环问题和数据覆盖的问题上。可以实现线程安全的容器,如 ConcurrentHashMap 或 Hashtable,或者是使用同步机制 synchronized 或 Lock 来保证其并发操作的安全性。


以上内容来自我的 《Java 面试突击训练营》,这门课程是 有着 14 年工作经验(前 360 开发工程师),9 年面试官经验的我,花费 4 年时间打磨完成的一门视频面试课

整个课程从 Java 基础到微服务 Spring Cloud、从实际开发问题到场景题应有尽有,如下图所示:

全程通过视频直播 + 录播的方式,把 Java 常见的面试题系统的过一遍,遇到一个问题,把这个问题相关的内容都给大家讲明白,并且视频支持永久更新和观看。

上完训练营的课程之后,基本可以应对目前市面上绝大部分公司的面试了,想要了解详情,加我微信:vipStone【备注:训练营】