认真研究JDK1.7下HashMap的循环链表和数据丢失问题

关联博文
数据结构之Map基础入门与详解
认真学习Java集合之HashMap的实现原理
认真研究HashMap的读取和存放操作步骤
认真研究HashMap的初始化和扩容机制
认真研究JDK1.7下HashMap的循环链表和数据丢失问题

本篇博文以jdk1.7为例分析。

前面博文认真研究HashMap的初始化和扩容机制我们详细分析了jdk1.7和Jdk1.8下HashMap的扩容机制。其中在jdk1.7下如果并发扩容,将会存在循环链表、数据丢失问题。本文我们详细描述这个过程。

我们再回顾一下resize方法:

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];
    boolean oldAltHashing = useAltHashing;
    useAltHashing |= sun.misc.VM.isBooted() &&
            (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = oldAltHashing ^ useAltHashing;
    transfer(newTable, rehash);
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

其中transfer是将旧的table数据转到新的table:

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;
         }
     }
 }

下面开始具体分析循环是如何出现的。

假设HashMap初始化大小为4,插入个3节点,不巧的是,这3个节点都hash到同一个位置,如果按照默认的负载因子的话,插入第4个节点就会扩容。
在这里插入图片描述
假设现在有两个线程同时进行,线程1和线程2,两个线程都会新建新的数组(数组扩容为2倍)。

也就是下面这句: Entry[] newTable = new Entry[newCapacity];
在这里插入图片描述
假设 线程2 在执行到 Entry<K,V>next=e.next;之后,cpu时间片用完了,这时线程2持有的变量e指向节点a,变量next指向节点b

线程1继续执行,很不巧,a、b、c节点rehash之后又是在同一个位置7,开始移动节点。

第一步,移动节点a(这里假设我们直接放链表中–为了图示说明):
在这里插入图片描述
第二步,移动节点b(jdk1.7插入到链表头部,jdk1.8插入到链表尾部):
在这里插入图片描述
第三步,继续移动节点c
在这里插入图片描述
假设这个时候线程1 的时间片用完,内部的table还没有设置成新的newTable, 线程2 开始执行,这时内部的引用关系如下(线程2 持有的变量e=a,next=b):
在这里插入图片描述
这时,在 线程2 中(如上图所示),变量e指向节点a,变量next指向节点b,开始执行循环体的剩余逻辑。

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;

执行之后的引用关系如下图(newTable[i]=a,a.next=null,e=b):
在这里插入图片描述
执行后,变量e指向节点b,因为e不是null,则继续执行循环体(如上图所示e.next此时为a,newTable[i]=b,b.next=a,e=next=a),执行后的引用关系图。
在这里插入图片描述
变量e又重新指回节点a,只能继续执行循环体,这里仔细分析下:
1、执行完 Entry<K,V> next=e.next;,目前节点a没有next(为null),所以变量next指向null;
2、 e.next=newTable[i]; 其中 newTable[i] 指向节点b,那就是把a的next指向了节点b,这样a和b就相互引用了,形成了一个环;
3、 newTable[i]=e 把节点a放到了数组i位置;
4、 e=next; 把变量e赋值为null,因为第一步中变量next就是指向null,将结束循环;

所以最终的引用关系是这样的:
在这里插入图片描述
节点a和b互相引用,形成了一个环。当在数组该位置get寻找对应的key时,就发生了死循环。

另外,如果线程2把newTable设置成内部的table,节点c的数据就丢了,看来还有数据遗失的问题。

总之,千万不要在多线程写时使用HashMap,单写多读是没有问题的。

参考博文:
HashMap死循环分析的修正版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值