鉴于jdk1.6及更高版本中的HashMaps会导致multi = threading问题,我应该如何修复我的代码

Sta*_*ura 83 java collections performance multithreading

我最近在stackoverflow中提出了一个问题,然后找到答案.最初的问题是除了互斥锁或垃圾收集之外的什么机制可以减缓我的多线程java程序?

我惊恐地发现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);
Run Code Online (Sandbox Code Playgroud)

最终打电话给谁

 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));
 }    
Run Code Online (Sandbox Code Playgroud)

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

对我的代码的影响是巨大的.它使得当我在64个线程上运行时,我获得的性能低于在1个线程上运行时的性能.一个JStack显示大多数线程花费大部分时间在Random循环中旋转.

所以我似乎有一些选择:

  • 重写我的代码,以便我不使用HashMap,但使用类似的东西
  • 不知何故搞乱了rt.jar,并替换其中的hashmap
  • 以某种方式与类路径混淆,因此每个线程都有自己的HashMap版本

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

谢谢您的帮助

Mik*_*gou 56

我是7u6中出现的补丁的原作者,CR#7118743:使用基于散列图的字符串替代哈希.

我会事先承认hashSeed的初始化是一个瓶颈,但它不是我们预期的问题,因为每个Hash Map实例只发生一次.要使此代码成为瓶颈,您必须每秒创建数百或数千个哈希映射.这当然不典型.您的申请是否真的有正当理由这样做?这些哈希映射存活多长时间?

无论如何,我们可能会调查切换到ThreadLocalRandom而不是Random,并且可能是cambecc建议的延迟初始化的一些变体.

编辑3

对瓶颈的修复被推送到JDK7更新mercurial repo:

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开发者可以看到它.

  • @mduigou具有高请求速率,并使用JSON的Web应用程序将创建大量的每秒包含HashMap的,因为大多数如果不是所有的JSON库使用包含HashMap或LinkedHashMaps反序列化JSON对象.使用JSON的Web应用是很广泛,并且包含HashMap的创作可能不是由应用程序(但应用程序使用的库)来控制,所以我会说,有正当理由创建包含HashMap时,没有一个瓶颈. (10认同)
  • 你的意思是"数十万"而不是"数百或数千"?- 很大的区别 (5认同)
  • @mduigou也许简单的缓解只是在调用CAS之前检查oldSeed是否相同.这种优化(称为测试测试和设置或TTAS)可能看起来是多余的,但在竞争中会产生重要的性能影响,因为如果CAS已经知道它将失败,则不会尝试CAS.CAS失败会产生令人遗憾的副作用,即将缓存行的MESI状态设置为Invalid - 要求所有各方从内存中重新检索该值.当然,Holger剥离种子是一个很好的长期修复,但即使这样,也应该使用TTAS优化. (3认同)
  • 迈克,一个近期解决方案的建议:除了ThreadLocalRandom(它会解决与线程本地存储混乱的应用程序有其自身的问题),它会更容易和更便宜(在时间,风险和测试方面)将Hashing.Holder.SEED_MAKER条带化为(例如)<num核心>随机实例的数组并使用调用线程的id将%-index导入其中?这应该立即缓解(但不是消除)每线程争用而没有任何明显的副作用. (2认同)

cam*_*ecc 30

这看起来像你可以解决的"bug".有一个属性禁用新的"替代哈希"功能:

jdk.map.althashing.threshold = -1
Run Code Online (Sandbox Code Playgroud)

但是,禁用替代散列是不够的,因为它不会关闭随机散列种子的生成(尽管它确实应该).因此,即使您关闭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);
Run Code Online (Sandbox Code Playgroud)

为什么这样做(可能)是安全的?由于已禁用alt散列,因此会忽略随机散列种子.因此,我们的实例Random实际上并不是随机的并不重要.像往常一样有这样令人讨厌的黑客,请谨慎使用.

(感谢/sf/answers/231120431/获取设置静态最终字段的代码).

---编辑---

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();
Run Code Online (Sandbox Code Playgroud)

可以使用类似的方法ConcurrentHashMap等.


eis*_*eis 2

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

这段代码是为了使哈希冲突更难引起,防止攻击者造成性能问题(详细信息) - 假设这个问题已经以其他方式处理,我认为你根本不需要同步。然而,无论您是否使用同步,您似乎都希望使用您自己的 Hashmap 版本,这样您就不会太依赖 JDK 提供的功能。

因此,您要么通常编写类似的内容并指向它,要么重写 JDK 中的类。要执行后者,您可以使用-Xbootclasspath/p:参数覆盖引导类路径。然而,这样做会“违反 Java 2 运行时环境二进制代码许可证”(来源)。