我有 32 个机器线程和一个ConcurrentHashMap<Key,Value> map,其中包含很多键。Key定义了一个公共方法visit()。我想visit()使用我可用的处理能力和可能的某种线程池来精确地映射每个元素。
我可以尝试的事情:
- 我可以使用该方法
map.keys()。结果Enumeration<Key>可以通过 using 进行迭代nextElement(),但由于调用key.visit()非常简短,我无法让线程保持忙碌。枚举本质上是单线程的。 - 我可以使用 synchronized
HashSet<Key>代替,调用一个方法toArray()并将数组上的工作拆分为所有 32 个线程。我严重怀疑此解决方案,因为该方法toArray()可能是单线程瓶颈。 - 我可以尝试继承 from
ConcurrentHashMap,获取其内部实例Segment<K,V>,尝试将它们分成 32 个组并分别处理每个组。不过,这听起来像是一种硬核方法。 - 或类似的魔法
Enumeration<Key>。
理想情况下:
- 理想情况下,a
ConcurrentHashMap<Key, Value>会定义一个方法keysEnumerator(int approximatePosition),这可能会让我失去一个缺少大约前 1/32 个元素的枚举器,即map.keysEnumerator(map.size()/32)
我错过了什么明显的东西吗?有没有人遇到过类似的问题?
编辑
我已经进行了分析,看看这个问题是否真的会影响实践中的性能。由于目前我无法访问集群,因此我使用笔记本电脑并尝试将结果外推到更大的数据集。visit()在我的机器上,我可以创建一个 200 万个键的 ConcurrentHashMap,并且在每个键上调用该方法需要大约 1 秒的时间来迭代它。该程序应该扩展到 8500 万键(及以上)。集群的处理器稍微快一些,但它仍然需要大约 40 秒来迭代整个地图。现在谈谈程序的逻辑流程。呈现的逻辑是顺序的,即在上一步中的所有线程完成之前,不允许任何线程进行下一步:
- 创建哈希映射,创建键并填充哈希映射
- 遍历访问所有键的整个哈希映射。
- 做一些数据混洗,即并行插入和删除。
- 重复步骤 2 和 3 数百次。
该逻辑流程意味着 40 秒的迭代将重复数百次,例如 100 次。这让我们仅在访问节点上花费了一个多小时。使用一组 32 个并行迭代器,它可以缩短到几分钟,这是一个显着的性能改进。
现在谈谈如何ConcurrentHashMap工作(或者我相信它是如何工作的)。每个ConcurrentHashMap由段组成(默认为 16)。对哈希映射的每次写入都会在相关段上同步。假设我们正在尝试将两个新键 k1 和 k2 写入哈希映射,并且它们将被解析为属于同一段,例如 s1。如果尝试同时写入它们,则其中一个将首先获取锁,然后再添加另一个。两个元素被解析为属于同一段的机会是多少?如果我们有一个好的散列函数和 16 个段,那么它就是 1/16。
我相信ConcurrentHashMap应该有一个方法concurrentKeys(),它将返回一个枚举数组,每个段一个。我有一些想法如何ConcurrentHashMap通过继承添加它,如果我成功了,我会告诉你的。就目前而言,解决方案似乎是创建一个 ConcurrentHashMaps 数组并预先散列每个键以解析为此类数组的一个成员。一旦准备好,我也会分享该代码。
编辑
这是不同语言的相同问题: