微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

ConcurrentHashMap无法按预期运行

如何解决ConcurrentHashMap无法按预期运行

我正在为电子选举投票,我的最初版本只有一个政党。每个选民将有不同的线程,这些线程将更新给定政党的投票计数。

我决定使用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",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吗?

解决方法

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

要解决此问题,您可以使用merge中定义的原子ConcurrentHashMap操作。整个方法调用是原子执行的。看起来就是这样。

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",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",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",0) + 1);分为三个步骤:

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。