线程安全是指在多线程(并发)环境下,多个线程同时操作同一个对象时,不会出现不符合预期的错误结果。然而 HashMap 却是线程不安全的,也就是当多个线程同时操作 HashMap 时,会出现不确定的错误结果。
为什么线程不安全?
HashMap 非线程安全主要是因为 HashMap 的设计中,未采用任何同步机制(锁机制)来保证其安全性。 这一点在它的源码注释中也有说明,如下图所示:
线程不安全问题展现
HashMap 线程不安全主要体现在以下两方面:
- 在 JDK 1.7 中的死循环问题
- 所有版本中的数据覆盖问题
死循环问题
死循环问题是指在并发环境下,因为多个线程同时进行 put 操作,导致链表形成环形数据结构,一旦形成环形数据结构,在 get(key) 的时候就会产生死循环。如下图所示:
死循环原因
HashMap 导致死循环的原因是由以下条件共同导致的:
- HashMap 使用头插法进行数据插入(JDK 1.8 之前);
- 多线程同时添加;
- 触发了 HashMap 扩容。
当满足以上所有条件时,HashMap 就会出现死循环问题。
数据覆盖问题
数据覆盖问题发生在并发添加元素的场景下,它不止出现在 JDK 1.7 版本中,其他版本中也存在此问题,数据覆盖产生的流程如下:
- 线程 T1 进行添加时,判断某个位置可以插入元素,但还没有真正的进行插入操作,自己时间片就用完了。
- 线程 T2 也执行添加操作,并且 T2 产生的哈希值和 T1 相同,也就是 T2 即将要存储的位置和 T1 相同,因为此位置尚未插入值(T1 线程执行了一半),于是 T2 就把自己的值存入到当前位置了。
- T1 恢复执行之后,因为非空判断已经执行完了,它感知不到此位置已经有值了,于是就把自己的值也插入到了此位置,那么 T2 的值就被覆盖了。
具体执行流程如下图所示。
数据覆盖执行步骤一
线程 T1 准备将数据 k1:v1 插入到 Null 处,但还没有真正的执行,自己的时间片就用完了,进入休眠状态了,如下图所示:
数据覆盖执行步骤二
线程 T2 准备将数据 k2:v2 插入到 Null 处,因为此处现在并未有值,如果此处有值的话,它会使用链式法将数据插入到下一个没值的位置上,但判断之后发现此处并未有值,那么就直接进行数据插入了,如下图所示:
数据覆盖执行步骤三
线程 T2 执行完成之后,线程 T1 恢复执行,因为线程 T1 之前已经判断过此位置没值了,所以会直接插入,此时线程 T2 插入的值就被覆盖了,如下图所示:
解决方案
HashMap 非线程安全的解决方案有以下几个:
- 使用线程安全容器 ConcurrentHashMap 替代 HashMap;
- 使用线程安全容器 Hashtable 替代 HashMap(此方式性能不高,不推荐使用);
- 在进行 HashMap 操作时,使用 synchronized 或 Lock 加锁执行。
小结
HashMap 线程不安全的主要原因,是因为 HashMap 的设计中未采用任何同步机制(锁机制)来保证其安全性。它的线程不安全主要表现在死循环问题和数据覆盖的问题上。可以实现线程安全的容器,如 ConcurrentHashMap 或 Hashtable,或者是使用同步机制 synchronized 或 Lock 来保证其并发操作的安全性。
特殊说明
以上内容来自我的《Java 面试突击训练营》,这门课程是有着十几年工作经验(前 360 开发工程师),10 年面试官经验的我,花费 4 年时间打磨完成的一门视频面试课。学完训练营的课程之后,基本可以应对目前市面上绝大部分公司的面试了,并且课程配备了 9 大就业服务,帮助上千人找到 Java 工作,其中上百人拿到大厂 Offer,学员最高薪资 70W 年薪,面试课目录和 9 大服务如下:
加我微信咨询:vipStone【备注:训练营】