简体   繁体   English

ConcurrentHashMap 无法按预期工作

[英]ConcurrentHashMap does not work as expected

I am counting votes for electronic election and I have only one party in my initial version.我正在计算电子选举的选票,并且在我的初始版本中我只有一个政党。 There will be different threads per voter and the threads will update the votes count of a given party.每个选民将有不同的线程,线程将更新给定政党的选票计数。

I decided to use ConcurrentHashMap, but the results are not what I expected...我决定使用 ConcurrentHashMap,但结果不是我所期望的......

Map<String, Integer> voting = new ConcurrentHashMap<>();

for (int i = 0; i < 16; i++) {
  new Thread(() -> {
    voting.put("GERB", voting.getOrDefault("GERB", 0) + 1);
  }).start();
}

for (int i = 0; i < 100; i++) {
  voting.put("GERB", voting.getOrDefault("GERB", 0) + 1);
}

Thread.sleep(5000); // Waits for the threads to finish

for (String s : voting.keySet()) {
  System.out.println(s + ": " + voting.get(s));
}

The result is different every time - it ranges from 114 to 116.结果每次都不同 - 它的范围从 114 到 116。

Isn't ConcurrentHashMap supposed to be synchronised? ConcurrentHashMap 不应该同步吗?

Well, there's a compound action here.嗯,这里有一个复合动作。 You get the map value given a key, increment it by one, and place it back in the map against the same key.您获得给定键的映射值,将其递增 1,然后将其放回映射中的同一个键。 You have to guarantee that all these statements execute atomically.您必须保证所有这些语句都以原子方式执行。 But the given implementation does not impose that prerequisite.但是给定的实现并没有强加这个先决条件。 Hence you end up with a safety failure.因此,您最终会遇到安全故障。

To fix this, you can use the atomic merge operation defined in ConcurrentHashMap .要解决此问题,您可以使用ConcurrentHashMap定义的原子merge操作。 The entire method invocation is performed atomically.整个方法调用以原子方式执行。 Here's how it looks.这是它的外观。

Map<String, Integer> voting = new ConcurrentHashMap<>();

for (int i = 0; i < 16; i++)
    new Thread(() -> {
        voting.merge("GERB", 1, Integer::sum);
    }).start();

for (int i = 0; i < 100; i++)
    voting.merge("GERB", 1, Integer::sum);

Thread.sleep(5000); // Waits for the threads to finish

for (String s : voting.keySet())
    System.out.println(s + ": " + voting.get(s));

Running this program produces the following output:运行此程序会产生以下输出:

GERB: 116日耳曼语:116

Assume there are two or more threads performs voting.put("GERB", voting.getOrDefault("GERB", 0) + 1);假设有两个或更多线程执行voting.put("GERB", voting.getOrDefault("GERB", 0) + 1);

what happens?发生什么了? Lets say value on key "GERB" now equals 10假设键“GERB”的值现在等于 10

  1. Thread #1 gets value voting.getOrDefault("GERB", 0) .线程 #1 获得值voting.getOrDefault("GERB", 0) It is 10它是 10
  2. Thread #2 gets value voting.getOrDefault("GERB", 0) .线程 #2 获取值voting.getOrDefault("GERB", 0) It is 10它是 10
  3. Thread #1 adds 1, now it is 11线程 #1 加 1,现在是 11
  4. Thread #2 adds 1, now it is 11线程 #2 加 1,现在是 11
  5. Thread #1 writes values 11 back to voting线程 #1 将值 11 写回voting
  6. Thread #2 writes values 11 back to voting线程 #2 将值 11 写回voting

Now, although 2 threads completes, the value increased only by 1 because of concurency.现在,虽然完成了 2 个线程,但由于并发,该值仅增加了 1。

So, yes, methods of ConcurrentHashMap are synchronized.所以,是的, ConcurrentHashMap方法是同步的。 That means, when one thread executes eg put , another thread waits.这意味着,当一个线程执行例如put ,另一个线程会等待。 But they do not synchronize threads outside anyhow.但是无论如何它们都不会同步外部线程。

If you perform several calls you have to synchronize them on your own.如果您执行多个呼叫,则必须自行同步它们。 Eg:例如:

final Map<String, Integer> voting = new ConcurrentHashMap<>();

for (int i = 0; i < 16; i++) {
  new Thread(() -> {
    synchronized (voting) { // synchronize the whole operation over the same object
       voting.put("GERB", voting.getOrDefault("GERB", 0) + 1);
    }
  }).start();
}

UPD As it noted in the comments, keep in mind that synchronization over voting object does not guarantee synchronization with ConcurentHahMap's methods itself. UPD正如评论中所指出的,请记住, voting对象上的同步并不能保证与 ConcurentHahMap 方法本身的同步。 You have to perform that synchronization for every call to voting methods if those calls can be performed concurrently.如果可以同时执行这些调用,则必须为每次调用voting方法执行该同步。 In fact, you can use any other object to synchronize (it's not required to be voting ): it only needs to be the same for all the threads.事实上,您可以使用任何其他对象进行同步(不需要voting ):只需要所有线程都相同即可。

But, as it noted by @Holger, this defeats the very purpose of the ConcurentHashMap .但是,因为它注意到@Holger,这违背了的根本目的ConcurentHashMap To utilize the atomic mechanics of ConcurentHashMap without locking the threads you can use method replace to retry the operation if the value was altered by another thread:要在不锁定线程的情况下利用ConcurentHashMap的原子机制,如果值被另一个线程更改,您可以使用方法replace重试操作:

for (int i = 0; i < 16; i++) {
  new Thread(() -> {
    Integer oldValue, newValue;
    do {
       oldValue = voting.getOrDefault("GERB", 0);
       newValue = oldValue + 1; // do some actions over the value
    } while (!voting.replace("GERB", oldValue, newValue)); // repeat if the value was changed
  }).start();
}

You can divide this line voting.put("GERB", voting.getOrDefault("GERB", 0) + 1);你可以把这条线分割成voting.put("GERB", voting.getOrDefault("GERB", 0) + 1); into three steps:分为三步:

int temp=voting.getOrDefault("GERB",0); //1
temp++;                                 //2
voting.put("GERB",temp);                //3

Now between line1 and line3, Other thread can change the value associated with "GERB" because the method has return, there is nothing can stop other thread from changing it.现在在第 1 行和第 3 行之间,其他线程可以更改与“GERB”关联的值,因为该方法已返回,没有什么可以阻止其他线程更改它。 So when you call voting.put("GERB",temp) ,you override their value which makes their update lost.因此,当您调用voting.put("GERB",temp) ,您会覆盖它们的值,这会使它们的更新丢失。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM