漫画:高并发下的HashMap

简介: 我们来讲解高并发环境下,HashMap可能出现的致命问题。

HashMap的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。

这时候,HashMap需要扩展它的长度,也就是进行Resize。

11.png

影响发生Resize的因素有两个:


1.Capacity


HashMap的当前长度。上一期曾经说过,HashMap的长度是2的幂。


2.LoadFactor


HashMap负载因子,默认值为0.75f。


衡量HashMap是否进行Resize的条件如下:


HashMap.Size   >=  Capacity * LoadFactor


1.扩容


创建一个新的Entry空数组,长度是原数组的2倍。


2.ReHash


遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。

让我们回顾一下Hash公式:

index =  HashCode(Key) &  (Length - 1)

当原数组长度为8时,Hash运算是和111B做与运算;新数组长度为16,Hash运算是和1111B做与运算。Hash结果显然不同。

Resize前的HashMap:

12.png

13.png

ReHash的Java代码如下:

/**
 * Transfers all entries from current table to newTable.
 */
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已经到了Resize的临界点。此时有两个线程A和B,在同一时刻对HashMap进行Put操作:

4.png

15.png

此时达到Resize条件,两个线程各自进行Rezie的第一步,也就是扩容:

16.png

这时候,两个线程都走到了ReHash的步骤。让我们回顾一下ReHash的代码:

22.png

假如此时线程B遍历到Entry3对象,刚执行完红框里的这行代码,线程就被挂起。对于线程B来说:

e = Entry3

next = Entry2

这时候线程A畅通无阻地进行着Rehash,当ReHash完成后,结果如下(图中的e和next,代表线程B的两个引用):

17.png

直到这一步,看起来没什么毛病。接下来线程B恢复,继续执行属于它自己的ReHash。线程B刚才的状态是:

e = Entry3

next = Entry2

23.png

当执行到上面这一行时,显然 i = 3,因为刚才线程A对于Entry3的hash结果也是3。


24.png

我们继续执行到这两行,Entry3放入了线程B的数组下标为3的位置,并且e指向了Entry2。此时e和next的指向如下:

e = Entry2

next = Entry2

整体情况如图所示:

18.png

接着是新一轮循环,又执行到红框内的代码行:

25.png

e = Entry2

next = Entry3


整体情况如图所示:

19.png

接下来执行下面的三行,用头插法把Entry2插入到了线程B的数组的头结点:

26.png

整体情况如图所示:

20.png

第三次循环开始,又执行到红框的代码:

27.png

e = Entry3

next = Entry3.next = null

最后一步,当我们执行下面这一行的时候,见证奇迹的时刻来临了:


28.png

newTable[i] = Entry2

e = Entry3

Entry2.next = Entry3

Entry3.next = Entry2

链表出现了环形!

整体情况如图所示:

21.png

此时,问题还没有直接产生。当调用Get查找一个不存在的Key,而这个Key的Hash结果恰好等于3的时候,由于位置3带有环形链表,所以程序将会进入死循环

1.Hashmap在插入元素过多的时候需要进行Resize,Resize的条件是

HashMap.Size   >=  Capacity * LoadFactor。

2.Hashmap的Resize包含扩容和ReHash两个步骤,ReHash在并发的情况下可能会形成链表环。

相关文章
|
Java 容器
Java——多线程高并发系列之ArrayList、HashSet、HashMap集合线程不安全的解决方案
Java——多线程高并发系列之ArrayList、HashSet、HashMap集合线程不安全的解决方案
Java——多线程高并发系列之ArrayList、HashSet、HashMap集合线程不安全的解决方案
|
Java
天猫面试官:说说高并发下的HashMap的死循环是怎么形成的!
师傅,我常常听别人说,不要在并发情况下使用HashMap,可能会出现死循环,这个死循环是怎么形成的呢?
125 0
|
安全 Java
【高并发】你敢信??HashMap竟然干掉了CPU!!
郑重声明:最近发现自己多年前写的一篇关于Race Condition引起的性能问题的文章,竟然被不少人直接拿来标记为“原创”了,真的很无语啊!故特此声明原创!
186 0
|
6月前
|
消息中间件 Java Linux
2024年最全BATJ真题突击:Java基础+JVM+分布式高并发+网络编程+Linux(1),2024年最新意外的惊喜
2024年最全BATJ真题突击:Java基础+JVM+分布式高并发+网络编程+Linux(1),2024年最新意外的惊喜
|
5月前
|
缓存 NoSQL Java
Java高并发实战:利用线程池和Redis实现高效数据入库
Java高并发实战:利用线程池和Redis实现高效数据入库
479 0
|
3月前
|
监控 算法 Java
企业应用面临高并发等挑战,优化Java后台系统性能至关重要
随着互联网技术的发展,企业应用面临高并发等挑战,优化Java后台系统性能至关重要。本文提供三大技巧:1)优化JVM,如选用合适版本(如OpenJDK 11)、调整参数(如使用G1垃圾收集器)及监控性能;2)优化代码与算法,减少对象创建、合理使用集合及采用高效算法(如快速排序);3)数据库优化,包括索引、查询及分页策略改进,全面提升系统效能。
46 0
|
5月前
|
存储 NoSQL Java
探索Java分布式锁:在高并发环境下的同步访问实现与优化
【6月更文挑战第30天】Java分布式锁在高并发下确保数据一致性,通过Redis的SETNX、ZooKeeper的临时节点、数据库操作等方式实现。优化策略包括锁超时重试、续期、公平性及性能提升,关键在于平衡同步与效率,适应大规模分布式系统的需求。
165 1
|
4月前
|
算法 Java 调度
高并发架构设计三大利器:缓存、限流和降级问题之使用Java代码实现令牌桶算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之使用Java代码实现令牌桶算法问题如何解决
|
4月前
|
监控 网络协议 Java
Java面试题:解释Java NIO与BIO的区别,以及NIO的优势和应用场景。如何在高并发应用中实现NIO?
Java面试题:解释Java NIO与BIO的区别,以及NIO的优势和应用场景。如何在高并发应用中实现NIO?
68 0
|
4月前
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
65 0

热门文章

最新文章

下一篇
无影云桌面