调整HashMap的大小:未来的危险

最近,我偶然发现了一个错误,该错误是由于多个线程对java.util.HashMap的使用不当引起的。 该错误是抽象泄漏的一个很好的例子。 只有了解数据结构的实现级别详细信息,才能帮助我解决当前的问题。 因此,我希望分享我所面临的问题将鼓励我们的一些读者熟悉基本数据结构的实现方式。

一天中,通常仅需几分钟即可完成的某些分析过程已经运行了数小时,因此,我所面对的症状每天都变得丑陋。 作为我们Craft.io的真正信奉者,我们通过自己的监控软件及时得到通知,并开始调查原因。

我也有几个来自处理线程的线程转储。 他们指出,该代码只是在堆转储中发现的哈希图中处理条目,看似处于未终止的循环中。 因此,似乎正在分析的数据以某种方式损坏了,其中包含循环引用。

令我惊讶的是,确实如此。 分析的堆内容中的HashMap条目相互引用。 在设计堆分析算法时,我们从未想到这是可能的。 显然我们错了。

由于已知HashMap实现不是线程安全的,所以我现在怀疑它与HashMap使用的并发问题有关。 实际上,在java.util.HashMap的设计中隐藏着一个问题。 如您所知, HashMap由存储区数组组成,每个存储区均引用一个链接的条目列表。 条目依次引用列表中的下一个条目,直到最后一个条目引用null:

调整HashMap的大小:未来的危险

我们的分析器遇到的问题是,两个条目相互引用形成一个封闭的循环。

调整HashMap的大小:未来的危险

在Google的帮助下,我发现了最终如何创建这样的循环引用是多线程环境中的一个问题。 再次提醒您, HashMap在运行时会根据映射中的条目数动态调整大小。 默认情况下, HashMaps使用75%的负载率。 这意味着只要地图中的条目数超过可用容量的75%,地图大小就会增加,以避免在地图元素条目上发生太多冲突。

所以我在这里。 显然,有多个线程试图同时调整地图的大小,从而在某些存储桶中创建了一个循环。 罪魁祸首最终隐藏在Java HashMap源代码的以下几行中:

void transfer(Entry[] newTable, boolean rehash) {
	... skipped for brevity ...
	Entry next = e.next;
	if (rehash) {
		e.hash = null == e.key ? 0 : hash(e.key);
	}
	... skipped for brevity ... 
}

现在,我们的分析端点解决方案非常简单。 我们只需要保留有关已处理条目的分类帐,而无需对所有条目进行两次处理就可以了。

我确实相信这是失败抽象的一个很好的例子。 Java中的HashMaps构建良好,即使您不了解实现细节,也可以很好地为您服务。 直到他们不这样做。 在这种情况下,对数据结构实现细节的深入了解将为您带来不同。

翻译自: https://www.javacodegeeks.com/2016/08/resizing-hashmap-dangers-ahead.html