Java 可扩展的方式访问ConcurrentHashMap的每个元素<;元素,布尔值>;正好一次

Java 可扩展的方式访问ConcurrentHashMap的每个元素<;元素,布尔值>;正好一次,java,concurrency,hashmap,bigdata,java.util.concurrent,Java,Concurrency,Hashmap,Bigdata,Java.util.concurrent,我有32个机器线程和一个ConcurrentHashMap,其中包含很多键Key定义了一个公共方法visit()。我想使用我现有的处理能力和可能的某种线程池,对map的每个元素执行一次visit() 我可以尝试的事情: 我可以使用方法map.keys()。产生的枚举可以使用nextElement()进行迭代,但由于对key.visit()的调用非常简短,因此我无法让线程保持忙碌。枚举本质上是单线程的 我可以使用一个同步的HashSet,调用一个方法toArray(),并将数组上的工作拆分为所有

我有32个机器线程和一个
ConcurrentHashMap
,其中包含很多键
Key
定义了一个公共方法
visit()
。我想使用我现有的处理能力和可能的某种线程池,对map的每个元素执行一次
visit()

我可以尝试的事情:

  • 我可以使用方法
    map.keys()
    。产生的
    枚举
    可以使用
    nextElement()
    进行迭代,但由于对
    key.visit()
    的调用非常简短,因此我无法让线程保持忙碌。枚举本质上是单线程的
  • 我可以使用一个同步的
    HashSet
    ,调用一个方法
    toArray()
    ,并将数组上的工作拆分为所有32个线程。我严重怀疑这种解决方案,因为方法
    toArray()
    很可能是单线程瓶颈
  • 我可以尝试从
    ConcurrentHashMap
    继承,获得其内部
    段的实例,尝试将它们分成32个组,并分别处理每个组。不过,这听起来像是一种硬核方法
  • 或类似于
    枚举的魔法
理想情况下:

  • 理想情况下,
    ConcurrentHashMap
    将定义一个方法
    KeyEnumerator(int-approximatePosition)
    ,该方法可能会使枚举器丢失大约前1/32个元素,即
    map.KeyEnumerator(map.size()/32)
我遗漏了什么明显的东西吗?以前有人遇到过类似的问题吗

编辑

我尝试过分析,看看这个问题实际上是否会影响实践中的性能。由于我目前无法访问集群,我使用笔记本电脑,并试图将结果推断到更大的数据集。在我的机器上,我可以创建一个200万个keys ConcurrentHashMap,在每个键上调用
visit()
方法,大约需要1秒的时间对其进行迭代。该程序预计可扩展到8500万键(及以上)。集群的处理器稍微快一点,但遍历整个映射仍需要大约40秒。现在我们来谈谈程序的逻辑流程。给出的逻辑是顺序的,即在前一步中的所有线程完成之前,不允许任何线程继续执行下一步:

  • 创建哈希映射,创建键并填充哈希映射
  • 遍历整个哈希映射,访问所有键。
  • 执行一些数据洗牌,即并行插入和删除。
  • 重复步骤2和3几百次。 这个逻辑流程意味着一个40秒的迭代要重复几百次,比如说100次。这给了我们一个多小时的时间来访问节点。使用一组32个并行迭代器,只需几分钟,这是一个显著的性能改进

    现在,让我们谈谈ConcurrentHashMap是如何工作的(或者我认为它是如何工作的)。每个
    ConcurrentHashMap
    都由段组成(默认为16)。对哈希映射的每次写入都在相关段上同步。假设我们试图将两个新键k1和k2写入哈希映射,它们将被解析为属于同一段,比如s1。如果试图同时写入它们,其中一个将首先获取锁,然后再提前添加另一个。两个元素被解析为属于同一段的可能性有多大?如果我们有一个好的散列函数和16段,它是1/16

    我相信
    ConcurrentHashMap
    应该有一个方法
    concurrentKeys()
    ,它将返回一个枚举数组,每个段一个。我有一些想法如何通过继承将其添加到
    ConcurrentHashMap
    ,如果成功,我会告诉您。目前的解决方案似乎是创建一个ConcurrentHashMaps数组,并对每个键进行预哈希,以解析为该数组的一个成员。一旦代码准备好了,我也会分享它

    编辑

    这是另一种语言中的相同问题:

    我可以尝试从ConcurrentHashMap继承,获得其内部部分的实例,尝试将它们分成32个组,并分别处理每个组。不过,这听起来像是一种硬核方法

    确实是硬核,但我认为唯一可行的办法是
    toArray()
    通过执行枚举来构建数组,这样就不会在那里获胜。我不相信同步的
    HashSet
    会更好,除非
    visit()
    运行与其他映射操作的比率相当高

    使用
    s的问题在于,您必须非常小心,确保代码具有弹性,因为我假设在您访问节点的同时,其他线程可能正在更改表,并且您需要避免不可避免的争用情况。当然很精致


    我心中的大问题是,这是否必要?是否有探查器或计时运行向您显示,访问一个线程中的每个键花费的时间过长?您是否尝试过为每个
    visit()
    调用创建一个线程池,并让一个线程执行枚举,池线程执行
    visit()

    如果我是您,我会先尝试迭代
    ConcurrentHashMap
    的键集。您可以尝试将密钥的处理传递到线程池(如果任务太轻,则以捆绑方式传递),甚至传递到ForkJoin任务,但只有在确实需要时才应该这样做

    已经说过,您可以使用
    ConcurrentSkipListMap
    ,在其中可以获得
    NavigableSet
    的密钥。你可以
    import scala.collection.parallel.mutable.ParHashMap
    
    class Node(value: Int, id: Int){
        var v = value
        var i = id
        override def toString(): String = v toString
    }
    
    object testParHashMap{
        def visit(entry: Tuple2[Int, Node]){
            entry._2.v += 1
        }
        def main(args: Array[String]){
            val hm = new ParHashMap[Int, Node]()
            for (i <- 1 to 10){
                var node = new Node(0, i)
                hm.put(node.i, node)
            }
    
            println("========== BEFORE ==========")
            hm.foreach{println}
    
            hm.foreach{visit}
    
            println("========== AFTER ==========")
            hm.foreach{println}
    
        }
    }