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

运行 该程序产生以下输出:

GERB: 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”关联的值,因为该方法有 return,没有什么可以阻止其他线程更改它。因此,当您调用 voting.put("GERB",temp) 时,您覆盖了它们的值,这使得它们的更新丢失。