提问者:小点点

HashMap与ConcurrentHashMap:线程之间的传输


我有一个关于在多线程应用程序中使用映射的问题。假设我们有这样的场景:

  1. 线程以列表的形式接收json数据

如您所见,map仅由单线程修改,但随后它“变成”只读(没有变化,只是不再修改)并传递给另一个线程。接下来,当我研究HasMap(也称为TreeMap)和Con电流tHashMap的实现时,后者具有易失性字段,而前两个没有。那么,在这种情况下我应该使用Map的哪个实现?Con电流tHashMap是过度选择还是由于线程间传输而必须使用?

我的简单测试表明,在同步修改HashMap/TreeMap时,我可以使用它们,并且可以正常工作,但我的结论或测试代码可能是错误的:

def map = new TreeMap() // or HashMap
def start = new CountDownLatch(1)
def threads = (1..5)
println("Threads: " + threads)
def created = new CountDownLatch(threads.size())
def completed = new CountDownLatch(threads.size())
threads.each {i ->
    new Thread({
        def from = i * 10
        def to = from + 10
        def local = (from..to)
        println(Thread.currentThread().name + " " + local)
        created.countDown()
        start.await()
        println('Mutating by ' + local)
        local.each {number ->
            synchronized (map) {
                map.put(number, ThreadLocalRandom.current().nextInt())
            }
            println(Thread.currentThread().name + ' added ' + number +  ': ' + map.keySet())
        }
        println 'Done: ' + Thread.currentThread().name
        completed.countDown()
    }).start()
}

created.await()
start.countDown()
completed.await()
println('Completed:')
map.each { e ->
    println('' + e.key + ': ' + e.value)
}

主线程生成5个子线程,同步更新公共映射,当它们完成主线程成功看到子线程的所有更新时。


共2个答案

匿名用户

java。util。并发类对排序有特殊保证:

内存一致性影响:与其他并发集合一样,在将对象放入阻塞队列之前,线程中的操作发生在另一个线程中访问或删除该元素之后的操作之前。

这意味着您可以自由地使用任何类型的可变对象,并根据需要对其进行操作,然后将其放入队列中。检索后,您应用的所有操作都将可见。

(请注意,更一般地说,您演示的那种测试只能证明缺乏安全性;在大多数实际情况下,99%的时间里,未同步代码都可以正常工作。最后1%的时间会让您头疼。)

匿名用户

这个问题的范围很广。

你说:

[A] 映射仅由单个线程修改,但随后它“变成”只读

棘手的部分是“然后”这个词。当程序员说“然后”时,你指的是“时钟时间”,例如我已经这样做了,现在这样做了。但是由于各种各样的原因,计算机不会这样“思考”(执行代码)。之前发生的事情和之后发生的事情需要“手动同步”,以便计算机以我们的方式看待世界。

这就是Java内存模型表达东西的方式:如果你希望你的对象在并发环境中的行为是可预测的,你必须确保你建立了“发生在”边界。

在java代码中,在建立关系之前会发生一些事情。简单一点,仅举几个例子:

  • 单个线程中的执行顺序(如果语句1和2是由同一个线程按该顺序执行的,那么语句2总是可以看到1所做的任何事情)
  • 当线程t1启动t2时,t2可以看到t1在启动t2之前所做的一切。与join()交互
  • 同步的对象监视器也是如此:同步块中的线程执行的每个操作都可以被在同一实例上同步的另一个线程看到
  • java的任何专门方法也是如此。util。并发类。e、 g锁和信号量,当然还有集合:如果将元素放入同步集合中,则取出它的线程在放入它的线程上有一个before
  • 如果T2在T1之前发生,如果T3在T2之前发生,那么T3也在T1之前发生

所以回到你的短语

然后它“变成”只读

它确实变成了只读的。但要让电脑看到它,你必须给“then”一个含义;也就是说:你必须在你的代码中把一个发生在关系之前的

稍后您会声明:

然后将列表放入阻塞队列

A<代码>java。util。并发队列?多么整洁啊!碰巧的是,从并发队列中拉出对象的线程与将所述对象放入队列的线程的repsect具有“发生在”关系。

你已经建立了关系。将对象放入队列的线程(之前)所做的所有突变都可以被拉出对象的线程安全地看到。在这种情况下,您不需要ConcurrentHashMap(当然,如果没有其他线程变异相同的数据)。

示例代码不使用队列。并且它会变异一个由多个线程修改的单一映射(而不是像您的场景中提到的那样)。所以,只是。。。不一样。但不管怎样,你的代码都很好。

访问地图的线程是这样做的:

synchronized (map) {
    map.put(number, ThreadLocalRandom.current().nextInt())
}

synchornize提供1)线程互斥,2)之前发生的情况。因此,每个进入同步的线程都可以在另一个也在其上进行同步的线程中看到“之前发生的”所有内容(这就是所有内容)。

所以这里没问题。

然后您的主线程执行:

completed.await()
println('Completed:')
map.each { e ->
   println('' + e.key + ': ' + e.value)
}

在这里保存您的内容已完成。等待()。这将为每个调用了countDown()的线程建立一个before,这就是所有线程。因此,主线程可以看到工作线程所做的一切。一切都很好。

除了...我们经常忘记检查线程的引导。第一次工作人员在map实例上同步时,以前没有人这样做过。我们怎么能确定他们看到一个map实例完全初始化并准备就绪。

嗯,有两个原因:

  1. 在调用线程之前初始化map实例。start(),它建立了一个“之前发生”。这将是足够的
  2. 在工作线程内部,您还可以在开始工作之前使用闩锁,然后再次建立关系

你加倍安全。