HashMap为什么线程不安全?

JDK1.7 及之前版本,在多线程环境下,HashMap 扩容时会造成死循环数据丢失的问题。

死循环问题:

这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,使得查询元素的操作陷入死循环而无法结束

为解决这个问题,JDK1.8版本的HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。

但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用 ConcurrentHashMap 。

 数据丢失问题在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK1.8 为例。

JDK1.8 后,在 HashMap 中,多个键值对可能会被分配到同一个桶,并以链表或红黑树的形式存储。多个线程对 HashMap put 操作会导致线程不安全,具体来说会有数据覆盖的风险。

举个例子:

  • 两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)
  • 不同的线程可能在不同的时间片获得 CPU执行的机会,当前现场 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。
  • 随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所以此时会直接进行插入,这就导致线程2插入的数据被线程 1 覆盖了。
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    // ...
    // 判断是否出现 hash 碰撞
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素(处理hash冲突)
    else {
    // ...
}

还有一种情况是这两个线程同时 put 操作导致 size 的值不正确,进而导致数据覆盖的问题:

  1. 线程 1 执行 if (++size > threshold) 判断时,假设获得 size 的值为 10,由于时间片耗尽挂起
  2. 线程 2 也执行 if (++size > threshold) 判断,获得 size 的值也为 10,并将元素插入到该桶中,并将 size 的值更新为 11.
  3. 随后,线程 1 获得时间片,它也将元素放入桶中,并将 size 的值更新为 11.
  4. 线程 1 , 2 都执行了一次 put 操作,但是 size 的值只增加了 1 ,也就导致实际上只有一个元素被添加到了 HashMap 中。
     
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    // ...
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

所以在并发操作中一般不使用 HashMap ,如果需要使用 Map 的话 可以使用 CurrentHashMap。

你可能感兴趣的:(Java,java,开发语言)