hashmap 1.7死循环问题

经常听人说起JDK1.7 HashMap死循环问题,一直没看过,今天试着看一下
HashMap数据结构主要有一个hash table(就一数组)和Node(每一个key-value组成一个Node)
hash表是数组,对于数组来说,读取任意位置的元素都是O(1),因为数组的首位置知道,读取任意下标的元素,只是偏移指定的位数,就是俗称的随机存取
如果hash碰撞,同一个hash值的Node会采用拉链式挂载在hash表同一个下标位置上,例如
hashmap
Node
为什么JDK1.7会有死循环的问题呢,主要是因为HashMap的扩容采用的是头插法,在多线程情况下会有线程安全问题
JDK1.7 HashMap的部分源码

public V put(K key, V value) {
	//如果hash表时空的,说明是第一次put,在HashMap中,
	//构造函数只是判断了初始容量,加载因子等参数的合法性,
	//hash表的初始化需要等第一次使用put的时候才会创建
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    //如果key为空,会放在hash table的下标为0的位置
    if (key == null)
        return putForNullKey(value);
    //计算hash值,这个采用了位异或运算,提高速度,同时保留了高低位,充分散列
    int hash = hash(key);
    //计算出来的hash值可能非常大,但是hash table只有那么点固定长度,
    //这里和hash table的length-1进行与运算,听说还有什么扰动函数,有点高深,
    //获得元素在数组中的下标位置
    int i = indexFor(hash, table.length);
    //遍历整个hash表,如果发现时相同的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;
        }
    }
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
	//如果size大于阈值(loadFactor*initalCapacity),而且位置上有元素,
	//对hash表进行扩容,length*2
    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;
    //如果老的hash表都=最大长度了,没法扩容了,直接退出
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = new Entry[newCapacity];
    //new出新的hash表之后,重新散列元素到新的hash表中
    //万恶之源
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    //转移完了,将引用指向新表,旧的就让jvm自己去回收了
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    //遍历整个旧的hash表
    for (Entry<K,V> e : table) {
    	//如果某个位置有元素
        while(null != e) {
        	//引用next指向这个位置的下一个元素
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //计算元素在新的hash表的位置下标
            int i = indexFor(e.hash, newCapacity);
            //元素的next指向新的hash表的下标为i的位置的元素
            e.next = newTable[i];
            //再把元素丢到新hash表的第i个位置
            //这就是头插入,先连后断
            newTable[i] = e;
            //元素的引用指向下一个位置,直到遍历完整个链表
            e = next;
        }
    }
}

这个不安全主要是多线程操作时,导致形成回环,进入死循环

这里假设两个线程T1和T2先后到了
resize的 Entry<K,V> next = e.next 位置,
此时T2因为某种原因阻塞了,T1继续往下transfer
此时大概是这样
Entry<K,V> next = e.next;
在这里插入图片描述
e.next = newTable[i];
因为原来的newTable没有值,所以指向null
在这里插入图片描述
newTable[i] = e;
在这里插入图片描述
e = next;
在这里插入图片描述
因为e不等于空,进入下一次循环
Entry<K,V> next = e.next;
B后面没有更多的元素了,指向的null,所以next也是指向null
在这里插入图片描述
e.next = newTable[i];
e.next指向newTable[i],newTable[i]就是A
在这里插入图片描述
newTable[i] = e;
在这里插入图片描述
e = next;
在这里插入图片描述
因为此时e=null了,T1完成任务,可以发现经过一轮扩容,A和B的指向相反了,这就是头插入导致的

此时T2唤醒了
此时引用指向是这样的,e2和next2依然分别指向A,B
在这里插入图片描述
此时B开始往table2往下走
e.next=newTable[i],这里的e是e2
因为newTable[i]是null,所以e.next还是null,不变
在这里插入图片描述
接着newTable[i]=e
将e指向的元素放在table2的第i个位置
在这里插入图片描述
e=next
在这里插入图片描述
此时e不为空,继续循环
next=e.next;
next指向了e的next,所以next=A
在这里插入图片描述
e.next=newTable[i]
e.next是B,B的next是A,newTable[i]也是A,所以没有变化…
在这里插入图片描述
newTable[i]=e
在这里插入图片描述
e=next
在这里插入图片描述
此时e=A,还是不为空继续循环,
next=e.next;
在这里插入图片描述
e.next=newTable[i]
因为newTable[i]=B,所以A的next指向了B
在这里插入图片描述
在这里插入图片描述
e=next
此时e指向了null,终于结束了循环
在这里插入图片描述
可以惊奇的发现,A和B构成了一个回环,下次Get或者Put操作时,将进入无线循环中,因为A的next指向了B,B的next又指向了A,…

好像JDK官方不承认这个是个bug,不提供fix,因为在源码的开头就注释了,多线程操作是不安全的,所有不安全操作都是用户导致的,所以拒绝修复,tomcat也因为这个原因限制了parameter的个数为10000个
不过好在jdk1.8已经修复了这个问题,这个死循环hashmap只能说成为过去了,但这个问题确实是很有趣

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值