HashMap并发问题

HashMap是非线程安全的,在并发情况下可能会在扩容时形成环形链表,导致死循环(该问题在JDK1.8版本已经修复),也可能发生数据丢失的问题

put源码(JDK7U71)

①:在HashMap构造函数中,并未显式指定table值,而是指向了EMPTY_TABLE(空数组),在put方法中进行初始化;
②:key=null时,键值对直接放在table[0]中,若已存在null的key,则替换并返回旧值,若不存在null,则放入链表头部;
③:在1.7中,扩容条件不止是阈值,只有当元素数量>=阈值,且目标桶存在元素的情况下才会进行扩容
④:扩容时,新数组的容量是旧数组的两倍;
⑤:扩容原理:创建新容量的数组,将旧数组的数据进行hash计算,放到新的数组中,最终将table指向新的数组;
以下代码来自JDK7U71版本

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {// ①
        inflateTable(threshold);// 如果table指向空数组,则初始化数组
    }
    if (key == null)
        return putForNullKey(value);// ② 空的键值对直接放到0桶
    // 计算所在桶的位置
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    // 检查是否有key相同的键值对,如果有则替换并返回旧值
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    // 操作数+1
    modCount++;
    addEntry(hash, key, value, i);// 添加键值对
    return null;
}

void addEntry(int hash, K key, V value, int bucketIndex) {
    // ③ 判断是否需要扩容(注:当元素数量>=阈值,且目标桶存在元素的情况下才会进行扩容)
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);// ④
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

void resize(int newCapacity) {
    Entry[] oldTable = table;
    // 计算新的容量
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    // ⑤
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));// initHashSeedAsNeeded是为了协助hash算法得到更好的散列值
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    // 遍历旧数组
    for (Entry<K,V> e : table) {
        // 遍历链表
        while(null != e) {
            Entry<K,V> next = e.next;// ⑥
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);

            e.next = newTable[i];// ⑦
            newTable[i] = e;
            e = next;
        }
    }
}

JDK7下的并发扩容

源码中计算桶位的算法比较复杂,不方便举例,此处以 index = XX % length代替,例:传入e10,此时index = 10 % 15 = 10,所以e10存放在table[10];

并发扩容案例

假设现有如下map,table中输入如图所示:
1
此时线程A.put(e23)B.put(e24),对应indexA=23%16=7,indexB=24%16=8,线程A、B均满足第③处代码的扩容条件;
假设线程B循环到e2,执行代码⑥后失去了CPU执行时间,此时B.e = 2,B.next = 34;
线程A获得执行时间,并执行完成transfer方法(注:源码是先扩容再加入新的元素,所以图中newTableA[23]还未放入元素),此时table如下:
2
通过上面的newTableA可以看到e34e2的关系如下:e34.next = e2
线程B重新获得执行时间,执行完当前循环,此时e2.next = null, B.e = e34, newTableB[2]=e2
开始下一轮循环,此时B.e = e34,执行完该轮循环:

B.next = B.e.next = e34.next = e2;// ⑥
B.e.next = newTableB[2] = e2;
newTable[2] = e34;
B.e = next = e2;

3
此时已经出现了端倪,B.e又回到了e2上,我们继续执行,再执行到第⑦处代码时,B.e.next = newTableB[2] = e.34,此时就出现了环形链表
4
程序继续向后执行,由于B.next = null,所以在newTableB[2]处的循环执行结束(注意:e50并没有遍历到),newTableB最终结果如下:
5
resize()方法中,线程A、B依次执行,会导致newTableA被newTableB覆盖(或newTableB被newTableA覆盖),若newTableA被newTableB覆盖,此时发生数据丢失:e50并没有遍历到,不会出现在newTableB中,数据发生丢失
完成扩容后,调用get(66)时(key=66,不存在且index(66)=2),此时会遍历table[2]中的链表,造成死循环问题。

并发扩容导致的问题

  • 数据丢失问题:扩容过程中形成环形链表,在环形链表之后的元素不会被遍历到,造成丢失(上例的e50);
  • 死循环:并发扩容形成环形链表,get(key)(key不存在且index(key)=环形链表所在桶)就会发生死循环问题;

JDK8

JDK8中HashMap采用数组+链表+红黑树的形式,在扩容时,新链表不再采用逆序插入的方式,而是保留头尾节点来完成正序插入,不会在链表处形成死循环;
看其他大佬的文章(HashMap在jdk1.8也会出现死循环的问题(实测)
)说JDK8中有可能会在链表与红黑树转换的过程中发生死循环,不过我没复现出来,不做争论;

总结

  • HashMap在并发场景下可能会出现死循环、数据丢失的问题;
  • HashMap在JDK7或8中都不是线程安全的类,并发场景下采用ConcurrentHashMap才是正解;

参考文章

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值