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


83

我最近在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版本

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

谢谢您的帮助


2
什么需要您创建那么多哈希图?你想做什么?
fge

3
2条评论:1. ConcurrentHashMap似乎不使用它-可以替代吗?2.仅在创建地图时调用这段代码。这意味着您要在竞争激烈的情况下创建数百万个哈希图-这真的反映了实际的生产负荷吗?
assylias 2012年

1
实际上,ConcurrentHashMap确实也使用了该方法(在oracle jdk 1.7_10中)-但显然openJDK 7并未使用
assylias

1
@assylias您应该在此处检查最新版本。这确实使用了这样的代码行。
Marko Topolnik

3
@StaveEscura认为AtomicLong写争用较低,可以正常工作。您的写争用较高,因此需要常规的排他锁定。编写一个同步HashMap工厂,您可能会看到一个改进,除非在这些线程中所做的只是映射实例化。
Marko Topolnik

Answers:


56

我是7u6 CR#7118743中出现的补丁的原始作者:使用基于哈希的Maps的字符串的替代哈希。

我会在最前面承认,hashSeed的初始化是一个瓶颈,但是我们认为这不是一个问题,因为每个Hash Map实例仅发生一次。为了使此代码成为瓶颈,您必须每秒创建数百或数千个哈希映射。这当然不是典型的。有没有真正为您的应用程序在做这个的正当理由?这些哈希图可以存活多长时间?

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

编辑3

解决瓶颈的问题已推送到JDK7更新程序库中:

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开发人员可以看到它。


1
感谢您查看这个。是的,确实确实需要制作许多地图:该应用程序实际上非常简单,但是每秒有10万人可以点击它,这意味着可以非常快速地创建数百万张地图。我当然可以将其重写为不使用地图,但这会带来很高的开发成本。目前,使用反射技术破解随机字段的计划看起来不错
Stave Escura 2013年

2
Mike,一个近期修复的建议:除了ThreadLocalRandom(它将在混乱线程本地存储的应用程序中有其自身的问题),它不会比在时间,风险和测试方面更容易和更便宜。将Hashing.Holder.SEED_MAKER分解为(例如)<num cores>个随机实例的数组,并使用调用线程的ID对其进行%-index?这应该立即缓解(尽管不能消除)每个线程的争用,而不会产生任何明显的副作用。
HolgerHoffstätte2013年

10
@mduigou具有高请求率并使用JSON的Web应用程序将每秒创建大量HashMap,因为大多数(如果不是全部)JSON库都使用HashMaps或LinkedHashMaps来反序列化JSON对象。使用JSON的Web应用程序很普遍,并且HashMaps的创建可能不受该应用程序控制(但是由库应用程序使用),因此,我说有充分的理由在创建HashMaps时不存在瓶颈。
sbordet

3
@mduigou也许一个简单的缓解方法就是在调用CAS之前先检查oldSeed是否相同。这种优化(称为测试-测试和设置或TTAS)似乎是多余的,但是在争用下可能会对性能产生重要影响,因为如果已经知道CAS将失败,则不会尝试CAS。失败的CAS有一个不幸的副作用,那就是将缓存行的MESI状态设置为“无效”-要求所有各方从内存中重新获取该值。当然,Holger的种子条带化是极好的长期解决方案,但即使如此,也应使用TTAS优化。
Jed Wesley-Smith

5
您是说“数十万”而不是“数十万”吗?-大差异
Michael Neale

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等。


1
谢谢。这确实是一个hack,但是可以暂时解决问题。这肯定是一个比我上面列出的列表更好的解决方案。长期而言,无论如何,我将不得不使用更快的HashMap来做一些事情。这让我想起了无法清除旧ResourceBundle缓存的解决方案。代码几乎相同!
斯塔夫·埃斯库拉

1
仅供参考,此处描述了该替代哈希功能:审阅请求CR#7118743:使用基于哈希的Map替代字符串的哈希。它是murmur3哈希函数的实现。
cambecc 2012年

3

在大数据应用程序中,有许多应用程序会为每个记录创建一个临时HashMap。例如,此解析器和序列化器。将任何同步放入未同步的集合类是一个真正的难题。我认为这是不可接受的,需要尽快解决。在7u6中显然引入的更改CR#7118743应该被还原或固定,而不需要任何同步或原子操作。

某种程度上,这使我想起了在JDK 1.1 / 1.2中使StringBuffer和Vector与HashTable同步的巨大错误。人们为这个错误付出了多年的高昂代价。无需重复该经验。


2

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

那里的那段代码使散列冲突更难以引起,从而防止了攻击者造成性能问题(详细信息)-假设已经以其他方式解决了此问题,我根本就不需要同步。但是,与是否使用同步无关,似乎您想使用自己的Hashmap版本,这样就不会在JDK所提供的功能上花很多的功夫。

因此,您通常只是编写类似的东西并指向它,或者重写JDK中的类。为此,您可以使用-Xbootclasspath/p:参数覆盖引导类路径。但是,这样做将“违反Java 2 Runtime Environment二进制代码许可证”()。


啊哈 我没有意识到那是优化的重点。非常聪明。我对攻击者的威胁模型不会使他们以这种方式弄乱哈希表,但我会在将来记住这一点。我同意您关于最终替换HashMap的观点。我可能必须将工厂对象或IOC容器线程化到构成它们的每个类中。我认为Cambecc给出的答案将使我脱颖而出,而我需要更长期的解决方案
Stave Escura 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.