0

2023版Java工程师

hghhy
1月前 9

获课:97it.top/17267/

#### 集合框架深潜:HashMap在扩容时的死循环与ConcurrentHashMap的优化

Java集合框架是每个开发者日常打交道最多的组件之一,其中HashMap以其高效的存取性能成为当之无愧的“主力军”。然而,在多线程环境下,这个看似强大的工具却潜藏着致命的危机——扩容时的死循环问题。这一问题曾让无数线上系统崩溃,也成为推动Java并发包(JUC)演进的重要动力。深入理解这一底层机制,以及ConcurrentHashMap如何通过精妙的设计化解危机,是每个Java工程师的必修课。

HashMap的底层结构是数组结合链表(或红黑树)。当元素数量超过阈值时,HashMap会触发扩容机制,将容量扩大为原来的两倍,并重新计算每个元素的位置,这一过程称为“rehash”。在JDK 1.7及之前版本,HashMap在扩容时采用的是头插法。这意味着在将原数组中的链表元素转移到新数组时,会将链表的节点顺序反转。这一设计在单线程环境下毫无问题,但在多线程并发扩容时,却埋下了巨大的隐患。

假设两个线程同时发现HashMap需要扩容,并且同时执行rehash操作。由于头插法的特性,它们在转移链表节点时,可能会互相覆盖对方的操作。具体来说,线程A可能在转移节点时,被线程B打断,而线程B完成了部分转移并修改了链表结构。当线程A恢复执行时,它基于已经被修改的链表结构进行操作,很可能将某个节点的next指针错误地指向了链表中的前驱节点,从而在链表中形成一个闭环。一旦链表成环,任何后续对该链表的遍历操作(如get、put)都将陷入无限循环,导致CPU利用率飙升至100%,最终使整个应用不可用。这就是HashMap在多线程环境下臭名昭著的“死循环”问题。

为了解决HashMap的线程安全问题,Java早期提供了HashTable和Collections.synchronizedMap等方案,但它们通过给整个方法加synchronized锁来实现同步,导致并发度极低,同一时刻只能有一个线程操作Map,成为性能瓶颈。随着JDK 1.5引入并发包(JUC),ConcurrentHashMap应运而生,它通过分段锁(Segment)机制,在保证线程安全的同时,极大地提升了并发性能。

在JDK 1.7中,ConcurrentHashMap将整个哈希表分成多个段(Segment),每个段独立加锁。不同线程可以同时操作不同的段,从而将锁的粒度从整个Map降低到段级别,大大提高了并发访问能力。每个Segment本质上是一个小型的HashMap,它继承自ReentrantLock,拥有独立的锁机制。当一个线程在操作某个Segment时,其他线程仍然可以访问和操作其他Segment,互不干扰。这种分而治之的策略,有效避免了HashMap扩容时可能出现的死循环问题,因为每个Segment的扩容是独立进行的,并且有锁保护。

到了JDK 1.8,ConcurrentHashMap的设计进一步优化,摒弃了Segment分段锁机制,转而采用CAS(Compare and Swap)操作结合synchronized关键字来实现更细粒度的同步。它将锁的粒度进一步降低到数组的每个桶(table[i])级别。当需要对某个桶进行写操作时,如果该桶为空,则使用CAS操作进行初始化;如果该桶非空,则使用synchronized关键字锁定该桶的头节点。这种设计在保证线程安全的前提下,最大限度地减少了锁的竞争,提升了并发性能。此外,JDK 1.8的HashMap在扩容时改用尾插法,从根本上解决了链表反转导致的成环问题,即使在多线程环境下,也不会再出现死循环。

从HashMap扩容的死循环到ConcurrentHashMap的不断演进,我们看到的是Java并发编程思想的深刻变革。它告诉我们,在高并发场景下,选择合适的工具至关重要。理解这些底层原理,不仅有助于我们编写出更健壮、高效的代码,更能让我们在面对复杂问题时,拥有更广阔的视野和更深刻的洞察。


本站不存储任何实质资源,该帖为网盘用户发布的网盘链接介绍帖,本文内所有链接指向的云盘网盘资源,其版权归版权方所有!其实际管理权为帖子发布者所有,本站无法操作相关资源。如您认为本站任何介绍帖侵犯了您的合法版权,请发送邮件 [email protected] 进行投诉,我们将在确认本文链接指向的资源存在侵权后,立即删除相关介绍帖子!
最新回复 (0)

    暂无评论

请先登录后发表评论!

返回
请先登录后发表评论!