HashMap死锁分析

Posted chanjuan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap死锁分析相关的知识,希望对你有一定的参考价值。

一、HashMap底层实现

简单的可以从以下两个纬度去理解HashMap的底层实现原理。

  • 数组:充当索引
  • 链表:处理碰撞

HashMap用一个指针数组table,离散化key的作用,当加入一个 key 的时候,通过Hash算法,计算出 key所在的数组下标 i,如果table[i]位置的对象元素为null的时候,则直接将<key, value>加入即可;但是,如果table[i]位置已经被占用的话,则会发生冲突碰撞;此时,会在 table[i]上形成一个链表。

如果table太小,就会发生频繁碰撞;此时,查询时间复杂度由O(1)变为O(n). 
因此,Hash 表的尺寸和容量非常重要。每次当有新的数据要插入Hash 表时,都会检查容量有没有超过 thredshold,如果超过,需要扩容 Hash 表,这需要改变重新计算hash分桶的位置—— rehash,这种操作是比较耗时的。

所以,在创建HashMap实例的时候需要预先估计一下需要处理的数据量的大小,提前将table的大小和装载因子load factor设置好,减少Hash碰撞的概率,同时也可以减少扩容hash表的次数,达到节约时间的目的。

二、源码阅读

当每次添加新元素都是在链表头部添加元素,那么,问题来了——为什么会造成死锁呢?按理说每次在链表头部添加元素的话,不可能出现死锁现象的。

问题就出在rehash过程,当将旧table元素转移到新的newTable的时候,我们一块来看看transfer()函数的源码,分析一下原因。

transfer()源码如下:

    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //Step1 : 首先便利索引数组中的元素,Entry<K,V> e 存储了链表的入口元素
        for (Entry<K,V> e : table) {
            //Step2: 对链表上的每一个元素进行遍历,从Hash表的头部第一个元素开始
            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;
            }
        }
    }

总结:

  1. 首先便利索引数组中的元素,获取到链表的入口节点
  2. 对链表上的每一个节点遍历:先将 e.next 指向新 Hash 表的第一个元素(如果是第一次就是 null),这时候新 Hash 的第一个元素是 e,但是 Hash 指向的却是 e 没转移时候的第一个,所以需要将 Hash 表的第一个元素指向 e.
  3. while循环遍历链表节点,直到全部转移到新的newTable
  4. for循环遍历table,可以理解为链表的入口头节点,直到所有索引数组全部转移到新的newTable

可以看到转移过程是逆序的,转移前链表顺序是1->2->3,逆序转移后新的t顺序变成 3->2->1。现在就应该才得八九不离十了,是不是有可能在转移的过程中 出现1>2>3>1这种情况,形成一个头尾相连的链表。

以上是关于HashMap死锁分析的主要内容,如果未能解决你的问题,请参考以下文章

怎么处理JAVA多线程死锁问题?

HashMap为啥会死锁

构建一个hashmap死锁的DEMO

什么是hashMap,初始长度,高并发死锁,java8 hashMap做的性能提升

大厂面试必问!HashMap 怎样解决hash冲突?

hashmap多线程不安全的原因