鉴于 jdk1.6 及更高版本中的 HashMaps 导致多线程问题,我应该如何修复我的代码

Posted

技术标签:

【中文标题】鉴于 jdk1.6 及更高版本中的 HashMaps 导致多线程问题,我应该如何修复我的代码【英文标题】:Given that HashMaps in jdk1.6 and above cause problems with multi=threading, how should I fix my code 【发布时间】:2012-12-10 06:13:48 【问题描述】:

我最近在***中提出了一个问题,然后找到了答案。最初的问题是What mechanisms other than mutexs or garbage collection can slow my multi-threaded java program?

我惊恐地发现 HashMap 在 JDK1.6 和 JDK1.7 之间进行了修改。它现在有一个代码块,可以使所有创建 HashMaps 的线程同步。

JDK1.7.0_10中的代码行是

 /**A randomizing value associated with this instance that is applied to hash code of  keys to make hash collisions harder to find.     */
transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);

最终调用

 protected int next(int bits) 
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do 
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
     while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
     

查看其他 JDK,我发现这在 JDK1.5.0_22 或 JDK1.6.0_26 中不存在。

对我的代码的影响是巨大的。它使得当我在 64 个线程上运行时,我得到的性能低于我在 1 个线程上运行时的性能。一个 JStack 表明,大多数线程的大部分时间都在 Random 的循环中旋转。

所以我似乎有一些选择:

重写我的代码,使我不使用 HashMap,而是使用类似的东西 不知何故弄乱了 rt.jar,并替换了其中的 hashmap 不知何故弄乱了类路径,所以每个线程都有自己的 HashMap 版本

在我开始使用这些路径中的任何一条之前(所有这些路径看起来都非常耗时且可能会产生很大影响),我想知道我是否错过了一个明显的技巧。你们中的任何人都可以堆栈溢出的人建议哪个是更好的路径,或者可能确定一个新想法。

感谢您的帮助

【问题讨论】:

是什么要求您创建这么多哈希图?你想做什么? 2 cmets: 1. ConcurrentHashMap 似乎没有使用它 - 它可以替代吗? 2. 这段代码只在地图创建时调用。这意味着您正在创建数百万个高度争用的哈希图 - 这真的反映了现实的生产负载吗? 实际上 ConcurrentHashMap 也使用该方法(在 oracle jdk 1.7_10 中) - 但显然 openJDK 7 does not. @assylias 你应该检查latest version over here。这个确实有这么一行代码。 @StaveEscura AtomicLong 押注低写入争用运行良好。你有很高的写争用,所以你需要定期排他锁定。编写一个同步的HashMap 工厂,您可能会看到改进,除非在这些线程中您所做的一切都是地图实例化。 【参考方案1】:

我是 7u6 中出现的补丁的原作者,CR#7118743 : Alternative Hashing for String with Hash-based Maps‌​。

我会提前承认 hashSeed 的初始化是一个瓶颈,但它不是我们预期的问题,因为它只在每个 Hash Map 实例中发生一次。要使此代码成为瓶颈,您必须每秒创建数百或数千个哈希映射。这当然不是典型的。您的应用程序这样做真的有正当理由吗?这些哈希图的寿命是多久?

无论如何,我们可能会研究切换到 ThreadLocalRandom 而不是 Random 以及 cambecc 建议的延迟初始化的一些变体。

编辑 3

已将瓶颈修复程序推送到 JDK7 更新 mercurial 存储库:

http://hg.openjdk.java.net/jdk7u/jdk7u-dev/jdk/rev/b03bbdef3a88

该修复将成为即将发布的 7u40 版本的一部分,并且已在 IcedTea 2.4 版本中提供。

7u40 的接近最终测试版本可在此处获得:

https://jdk7.java.net/download.html

仍然欢迎反馈。将其发送至http://mail.openjdk.java.net/mailman/listinfo/core-libs-dev 以确保它被 openJDK 开发人员看到。

【讨论】:

感谢您对此进行调查。是的,确实需要制作这么多地图:该应用程序实际上非常简单,但每秒可以有 100,000 人点击它,这意味着可以非常快速地创建数百万张地图。我当然可以重写它以不使用地图,但开发成本非常高。目前,使用反射破解 Random 字段的计划看起来不错 Mike,对近期修复的建议:除了 ThreadLocalRandom (它会与应用程序与线程本地存储混淆)之外,它不会更容易和更便宜(就时间、风险和测试)将 Hashing.Holder.SEED_MAKER 条带化到(比如说) 随机实例的数组中,并使用调用线程的 id 来 %-index 进入它?这应该会立即缓解(但不能消除)每个线程的争用,而不会产生任何明显的副作用。 @mduigou 具有高请求率并使用 JSON 的 Web 应用程序将每秒创建大量 HashMap,因为大多数(如果不是全部)JSON 库都使用 HashMap 或 LinkedHashMap 来反序列化 JSON 对象。使用 JSON 的 Web 应用程序很普遍,并且 HashMaps 的创建可能不受应用程序控制(而是由库应用程序使用),所以我想说在创建 HashMaps 时没有瓶颈是有正当理由的。 @mduigou 也许一个简单的缓解方法就是在调用 CAS 之前检查 oldSeed 是否相同。这种优化(称为 test-test and set 或 TTAS)可能看起来是多余的,但在争用情况下可能会对性能产生重要影响,因为如果 CAS 已经知道它会失败,则不会尝试它。失败的 CAS 具有将缓存行的 MESI 状态设置为无效的不幸副作用——要求所有各方从内存中重新检索值。当然,Holger 的种子条带化是一个很好的长期解决方案,但即便如此,也应该使用 TTAS 优化。 您的意思是“数十万”而不是“数百或数千”? - 大不同【参考方案2】:

这看起来像是一个可以解决的“错误”。有一个属性会禁用新的“替代散列”功能:

jdk.map.althashing.threshold = -1

但是,禁用替代散列是不够的,因为它不会关闭随机散列种子的生成(尽管它确实应该这样做)。因此,即使您关闭了 alt 哈希,在哈希映射实例化期间仍然存在线程争用。

解决此问题的一种特别讨厌的方法是用您自己的非同步版本强制替换用于生成哈希种子的Random 实例:

// Create an instance of "Random" having no thread synchronization.
Random alwaysOne = new Random() 
    @Override
    protected int next(int bits) 
        return 1;
    
;

// Get a handle to the static final field sun.misc.Hashing.Holder.SEED_MAKER
Class<?> clazz = Class.forName("sun.misc.Hashing$Holder");
Field field = clazz.getDeclaredField("SEED_MAKER");
field.setAccessible(true);

// Convince Java the field is not final.
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);

// Set our custom instance of Random into the field.
field.set(null, alwaysOne);

为什么(可能)这样做是安全的?因为 alt 散列已被禁用,导致随机散列种子被忽略。所以我们的Random 实例实际上不是随机的并不重要。像往常一样令人讨厌的黑客攻击,请谨慎使用。

(感谢https://***.com/a/3301720/1899721 提供设置静态最终字段的代码)。

--- 编辑---

FWIW,对 HashMap 的以下更改将在禁用 alt 散列时消除线程争用:

-   transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);
+   transient final int hashSeed;

...

         useAltHashing = sun.misc.VM.isBooted() &&
                 (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
+        hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0;
         init();

类似的方法可以用于ConcurrentHashMap

【讨论】:

谢谢。这确实是一个hack,但它暂时解决了问题。这肯定是比我上面列出的任何一个更好的解决方案。无论如何,从长远来看,我将不得不使用更快的 HashMap 做一些事情。这让我想起了旧 ResourceBundle 缓存不可清除的解决方案。代码几乎完全相同! 仅供参考,此处描述了此 alt 哈希功能:Review Request CR#7118743 : Alternative Hashing for String with Hash-based Maps。它是 murmur3 哈希函数的实现。【参考方案3】:

有很多应用程序在大数据应用程序中为每条记录创建一个瞬态 HashMap。例如,这个解析器和序列化器。将任何同步放入非同步集合类是一个真正的问题。在我看来,这是不可接受的,需要尽快修复。显然在 7u6 中引入的更改,CR#7118743 应该被还原或修复,而不需要任何同步或原子操作。

这让我想起了在 JDK 1.1/1.2 中使 StringBuffer 和 Vector 和 HashTable 同步的巨大错误。多年来,人们为这个错误付出了沉重的代价。无需重复这种体验。

【讨论】:

【参考方案4】:

假设您的使用模式是合理的,您会想要使用自己的 Hashmap 版本。

那段代码使哈希冲突更难引起,防止攻击者制造性能问题 (details) - 假设这个问题已经以其他方式处理,我认为你不会完全需要同步。但是,无论您是否使用同步,您似乎都希望使用自己的 Hashmap 版本,这样您就不会过多地依赖 JDK 提供的功能。

所以要么你通常只写一些类似的东西并指向它,要么覆盖 JDK 中的一个类。要执行后者,您可以使用 -Xbootclasspath/p: 参数覆盖引导类路径。但是,这样做会“违反 Java 2 运行时环境二进制代码许可证”(source)。

【讨论】:

啊哈。我没有意识到这是优化的重点。非常聪明。我的攻击者威胁模型没有让他们以这种方式弄乱哈希图,但我会记住这一点以备不时之需。我同意你关于最终替换 HashMap 的观点。我可能不得不将一个工厂对象或一个 IOC 容器线程化到创建它们的每个类中。我认为 Cambecc 给出的答案会让我摆脱困境,同时我正在研究更长期的解决方案

以上是关于鉴于 jdk1.6 及更高版本中的 HashMaps 导致多线程问题,我应该如何修复我的代码的主要内容,如果未能解决你的问题,请参考以下文章

Kafka 0.9 及更高版本中的 Zookeeper 故障

flywaydb中的数据库基线版本是啥。我可以使用它从特定版本及更高版本进行迁移吗?

Android 6 及更高版本中的 Android SSLSocket 握手失败

Java中HashMap底层实现原理(JDK1.8)源码分析

Java中HashMap底层实现原理(JDK1.8)源码分析

Java开发大牛用代码演示基于JDK1.6版本下的HashMap详解