我有 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 数组并预先散列每个键以解析为此类数组的一个成员。一旦准备好,我也会分享该代码。
编辑
这是不同语言的相同问题: