繁体   English   中英

ConcurrentHashMap 无法按预期工作

[英]ConcurrentHashMap does not work as 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));
}

结果每次都不同 - 它的范围从 114 到 116。

ConcurrentHashMap 不应该同步吗?

嗯,这里有一个复合动作。 您获得给定键的映射值,将其递增 1,然后将其放回映射中的同一个键。 您必须保证所有这些语句都以原子方式执行。 但是给定的实现并没有强加这个先决条件。 因此,您最终会遇到安全故障。

要解决此问题,您可以使用ConcurrentHashMap定义的原子merge操作。 整个方法调用以原子方式执行。 这是它的外观。

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));

运行此程序会产生以下输出:

日耳曼语:116

假设有两个或更多线程执行voting.put("GERB", voting.getOrDefault("GERB", 0) + 1);

发生什么了? 假设键“GERB”的值现在等于 10

  1. 线程 #1 获得值voting.getOrDefault("GERB", 0) 它是 10
  2. 线程 #2 获取值voting.getOrDefault("GERB", 0) 它是 10
  3. 线程 #1 加 1,现在是 11
  4. 线程 #2 加 1,现在是 11
  5. 线程 #1 将值 11 写回voting
  6. 线程 #2 将值 11 写回voting

现在,虽然完成了 2 个线程,但由于并发,该值仅增加了 1。

所以,是的, ConcurrentHashMap方法是同步的。 这意味着,当一个线程执行例如put ,另一个线程会等待。 但是无论如何它们都不会同步外部线程。

如果您执行多个呼叫,则必须自行同步它们。 例如:

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正如评论中所指出的,请记住, voting对象上的同步并不能保证与 ConcurentHahMap 方法本身的同步。 如果可以同时执行这些调用,则必须为每次调用voting方法执行该同步。 事实上,您可以使用任何其他对象进行同步(不需要voting ):只需要所有线程都相同即可。

但是,因为它注意到@Holger,这违背了的根本目的ConcurentHashMap 要在不锁定线程的情况下利用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();
}

你可以把这条线分割成voting.put("GERB", voting.getOrDefault("GERB", 0) + 1); 分为三步:

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

现在在第 1 行和第 3 行之间,其他线程可以更改与“GERB”关联的值,因为该方法已返回,没有什么可以阻止其他线程更改它。 因此,当您调用voting.put("GERB",temp) ,您会覆盖它们的值,这会使它们的更新丢失。

暂无
暂无

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

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