HashMap原理,以及HashMap的非线程安全

3 篇文章 0 订阅
HashMap是一种基于哈希表实现的Map,通过键的hashCode确定存储位置,可能存在键值冲突时形成链表。然而,HashMap在多线程环境下进行resize操作可能导致死循环,因为resize时使用头插法可能导致环形链表的产生。单线程的resize过程不会出现问题,但在并发情况下,两个线程同时resize可能导致链表结构混乱,形成环状,从而引发线程安全问题。
摘要由CSDN通过智能技术生成

          1、HashMap简单描述:

           1.1、HashMap是基于哈希表的Map接口实现,支持null值和null键,根据key的hashCode来确定元素的存储位置,不支持重复的键

          1.2、当计算出的hashCode位置已经有元素时,HashMap采用链地址法在其后面追加一个新的节点;

  1.3、在根据key来get元素是,如果之前存入的hashCode有冲突,会遍历bucket中的Entry链,保证e.hash==hash && ((k = e.key) == key || (key != null && key.equals(k));

         1.4、Entry对象:HashMap其实相当于是一个Entry数组,Entry包含键和值,当hash冲突时,形成链表。

         2、HashMap的线程不安全

都说HashMap是线程不安全的,到底为什么不安全呢。看了jdk源码就会明白,原来是多线程resize()操作的时候可能会造成死循环,下面就来研究下,多线程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];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        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;
            }
        }
}

我们都知道HashMap的默认容量是16,在插入元素的时候先要计算有没有超过thredhold,如果超过要增大hash表的尺寸,进行rehash。性能开销还是比较大的。

      先看看transfer方法主要执行的操作吧:

 2.1、对索引数组中元素进行遍历

 2.2、对链表遍历上面的每一个节点:用 next 取得要转移那个元素的下一个,将 e 转移到新 Hash 表的头部,使用头插法插入节点

 2.3、循环2,直到链表节点全部转移

 2.4、循环1,直到所有索引数组全部转移

        会发现转移的时候是逆序的,加入之前链表顺序是A-B-C,转移完成后会变成C-B-A。这个时候就会发现死锁可能是因为A-B的时候B-A造成的。


2.5 单线程安全的resize()

假设最开始hash表的长度是2,hashCode=value mod length;

                接下来是把Hash表resize到4,并将所有<key,value>重新rehash到新hash表的过程

               

                  可以看出单线程的resize并不会出错

               2.6 多线程resize演示

提出关键部分代码

while(null != e) {
    Entry<K,V> next = e.next;
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
}


              

                        1)Entry<K,V>  next = e.next; 单链表,记住头节点

2)e.next = newTable[i]; e要插到链表的头部(复杂度O(1),不插到尾部的原因(O(N))),所以要先用e.next指向新的hash表的第一个元素;

3)newTable[i] = e; 将新Hash表的头指针指向e;

4)e = next;转移e的下一个节点

假设这里有两个线程同时执行了 put() 操作,并进入了 transfer() 环节

while(null != e) {
    Entry<K,V> next = e.next; //线程1执行到这里被调度挂起了
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
}

那么现在的状态为:

从上面的图我们可以看到,因为线程1的 e 指向了 key(3),而 next 指向了 key(7),在线程2 rehash 后,就指向了线程2 rehash 后的链表。

然后线程1被唤醒了:

  1. 执行 e.next = newTable[i] ,于是 key(3)的 next 指向了线程1的新 Hash 表,因为新 Hash 表为空,所以 e.next = null ,
  2. 执行 newTable[i] = e ,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash 表的 key(3)。好了,e 处理完毕。
  3. 执行 e = next ,将 e 指向 next,所以新的 e 是 key(7)

然后该执行 key(3)的 next 节点 key(7)了:

  1. 现在的 e 节点是 key(7),首先执行 Entry<K,V> next = e.next ,那么 next 就是 key(3)了
  2. 执行 e.next = newTable[i] ,于是key(7) 的 next 就成了 key(3)
  3. 执行 newTable[i] = e ,那么线程1的新 Hash 表第一个元素变成了 key(7)
  4. 执行 e = next ,将 e 指向 next,所以新的 e 是 key(3)

这时候的状态图为:

然后又该执行 key(7)的 next 节点 key(3)了:

  1. 现在的 e 节点是 key(3),首先执行 Entry<K,V> next = e.next ,那么 next 就是 null
  2. 执行 e.next = newTable[i] ,于是key(3) 的 next 就成了 key(7)
  3. 执行 newTable[i] = e ,那么线程1的新 Hash 表第一个元素变成了 key(3)
  4. 执行 e = next ,将 e 指向 next,所以新的 e 是 key(7)

这时候的状态如图所示:

很明显,环形链表出现了!!当然,现在还没有事情,因为下一个节点是 null,所以 transfer() 就完成了,等 put() 的其余过程搞定后,HashMap 的底层实现就是线程1的新 Hash 表了。


   

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值