考虑到jdk1.6及以上版本中的hashmap会导致多线程问题,我应该如何修复我的代码?



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

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

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

在我开始这些路径之前(所有看起来都非常耗时和潜在的高影响),我想知道我是否错过了一个明显的技巧。你们中的任何一个栈溢出的人可以建议哪个是更好的路径,或者可能提出一个新的想法。

感谢您的帮助

我是出现在6月6日的补丁的原作者,CR#7118743:使用基于哈希的映射的字符串的替代哈希。

我将承认hashSeed的初始化是一个瓶颈,但它不是一个我们预期的问题,因为它只发生一次每个哈希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开发人员可以看到它。

这看起来像是一个可以解决的"bug"。有一个属性可以禁用新的"可选散列"特性:

jdk.map.althashing.threshold = -1

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

解决这个问题的一个特别糟糕的方法是用你自己的非同步版本强制替换用于生成哈希种子的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);

为什么这样做(可能)是安全的?因为所有散列已被禁用,导致随机散列种子被忽略。所以Random的实例不是随机的并不重要。与往常一样,请谨慎使用。

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

—Edit—

FWIW,以下更改HashMap将消除线程争用时,所有哈希被禁用:

-   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

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

这让我想起了在JDK 1.1/1.2中使StringBuffer、Vector和HashTable同步的巨大错误。人们为这个错误付出了多年的代价。没有必要再来一次。

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

这段代码是为了使哈希冲突更难引起,防止攻击者产生性能问题(细节)——假设这个问题已经用其他方式解决了,我认为你根本不需要同步。然而,如果你使用同步与否无关紧要,似乎你会想要使用自己的Hashmap版本,这样你就不会依赖于JDK碰巧提供的东西。

所以你要么写一些类似的东西并指向它,要么在JDK中重写一个类。要实现后者,可以使用-Xbootclasspath/p:参数覆盖引导类路径。然而,这样做将"违反Java 2运行时环境二进制代码许可"(源代码)。

最新更新