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

我最近在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);

这最终会调用

 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版本

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

感谢您的帮助


答案 1

我是出现在7u6,CR#7118743中的补丁的原始作者:带有基于哈希的Map的字符串的替代哈希。

我会预先承认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 开发人员可以看到它。


答案 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://stackoverflow.com/a/3301720/1899721 设置静态最终字段的代码)。

--- 编辑---

FWIW,以下更改将在禁用 alt 哈希时消除线程争用: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


推荐