• 为什么说HashMap是非线程安全的?
  • 发布于 2个月前
  • 221 热度
    0 评论

线程安全是指在多线程(并发)环境下,多个线程同时操作同一个对象时,不会出现不符合预期的错误结果。然而 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 来保证其并发操作的安全性。


用户评论