Java Collection集合类 - HashMap 可能出现的死循环问题

问题描述

问题 : 服务器cpu被100%的线上故障,是因为java语言在并发情况下使用了HashMap造成Receive Condition (竞态条件 : 并发访问出现冲突,结果不正确)

首先HashMap是非线程安全的数据结构,在单线程的程序中使用,是没有问题的.但是在多线程程序中,就可能出现问题.

HashMap回顾

  • HashMap 通常会使用一个数组(table[] 来分散所有的key),当一个key被加入时 ,会通过hash算法会通过key计算出key要放入数组位置的下标(i),然后将<key,value> 插到tablep[i] 中 , 如果两个key算出来同一个i,那么这就叫冲突,在HashMap中采用链地址法解决冲突,因而在数组的每个节点的下面会形成一个链表.
  • table[]的初始大小很小,只有16,但是需要添加大量数据时,碰撞就会变得频繁.所以就规定了一个阈值(thredhold),如果超过就需要扩大table[]的大小,但是, 整个hash表里面的元素位置都要重新计算一遍(rehash),这个花费的成本相当大.

rehash源码

添加一个<key,value>到Hash表中
public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key); 
		// 计算i
        int i = indexFor(hash, table.length);
		//判断添加的key是否在链表中,如果已经存在,替换掉原来的value 
        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是否大于阈值和计算出的i位置是否为空,满足则需要resize 
        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);
    }
新建hash表,将旧表中的数据转移至新表中
   void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
		// 创建新的table		
        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
		// 将旧table的数据转移到新的table中
        transfer(newTable, rehash);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
迁移源码
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //遍历oldtable中的每一个元素,将其放入newtable中
        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);
                //此处每次都将新节点写入到 newTable[i] 中,用e.next将先前写入的节点保存起来
                e.next = newTable[i]; // 保存先前写入的节点
                newTable[i] = e; // 将节点写入 newTable[i] = e
                e = next;
            }
        }
    }

正常情况的rehash

  • 首先最开始的size 大小为2 ,简单的用 key mod size 算出其 i 值
  • 接下来将hash表resize为 4 ,将所有的<key,value >重新rehash
    在这里插入图片描述

并发下的rehash

  1. 我们有两个进程,进程1被阻塞到如下位置
           do {
                 Entry<K,V> next = e.next;   //当线程1执行到这里被调度挂起
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
    

进程二执行完了
在这里插入图片描述
因为Thread1 的e 指向了 key :3 ,而next 指向了key :7 .而在线程二rehash1 ,Thread1的e指向了key:7,next指向了key: 3
2. 线程1被调度执行

  • 首先执行的是 newTable[i] = e ; (null)
  • 然后是e = next ; (导致e指向 key : 7)
  • 而下一次的循环中 next指向了key : 3
    在这里插入图片描述
  1. 将key :7 ,放到newTable[i]的第一个位置,然后将e 和next往下移
    在这里插入图片描述
  2. 环形链接出现
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值