82

我最近在stackoverflow中提出了一个问题,然后找到了答案。最初的问题是除了互斥锁或垃圾回收之外,还有哪些机制会减慢我的多线程 Java 程序的速度?

我惊恐地发现 HashMap 已经在 J​​DK1.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 显示大多数线程将大部分时间花在随机循环中。

所以我似乎有一些选择:

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

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

谢谢您的帮助

4

4 回答 4

56

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

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

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

编辑 3

对瓶颈的修复已被推送到 JDK7 update 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 开发人员看到。

于 2012-12-27T19:58:56.713 回答
30

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

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://stackoverflow.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等。

于 2012-12-23T16:22:54.897 回答
3

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

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

于 2013-01-05T21:24:04.107 回答
2

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

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

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

于 2012-12-23T13:55:09.680 回答