ConcurrentHashMap不按预期工作。

huangapple go评论91阅读模式
英文:

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); // 等待线程完成

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

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

ConcurrentHashMap 不是应该是同步的吗?

英文:

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...

Map&lt;String, Integer&gt; voting = new ConcurrentHashMap&lt;&gt;();

for (int i = 0; i &lt; 16; i++) {
  new Thread(() -&gt; {
    voting.put(&quot;GERB&quot;, voting.getOrDefault(&quot;GERB&quot;, 0) + 1);
  }).start();
}

for (int i = 0; i &lt; 100; i++) {
  voting.put(&quot;GERB&quot;, voting.getOrDefault(&quot;GERB&quot;, 0) + 1);
}

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

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

The result is different every time - it ranges from 114 to 116.

Isn't ConcurrentHashMap supposed to be synchronised?

答案1

得分: 12

好的,以下是您要翻译的内容:

有一个复合操作在这里。给定一个键,您可以获取映射值,将其递增一,并将其放回映射中的同一键。您必须确保所有这些语句都以原子方式执行。但是给定的实现并没有强制执行这个先决条件。因此,您最终会出现安全故障。

要解决这个问题,您可以使用在“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); // 等待线程完成

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

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

> GERB: 116
英文:

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. 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. The entire method invocation is performed atomically. Here's how it looks.

Map&lt;String, Integer&gt; voting = new ConcurrentHashMap&lt;&gt;();

for (int i = 0; i &lt; 16; i++)
	new Thread(() -&gt; {
		voting.merge(&quot;GERB&quot;, 1, Integer::sum);
	}).start();

for (int i = 0; i &lt; 100; i++)
	voting.merge(&quot;GERB&quot;, 1, Integer::sum);

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

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

Running this program produces the following output:

> GERB: 116

答案2

得分: 3

假设有两个或更多线程执行以下操作:

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) { // 对相同对象进行整个操作的同步
       voting.put("GERB", voting.getOrDefault("GERB", 0) + 1);
    }
  }).start();
}

更新
正如评论中所指出的,要注意的是,对 voting 对象的同步并不保证与 ConcurrentHashMap 方法本身的同步。如果这些调用可以并发执行,你必须为每个调用执行同步。事实上,你可以使用任何其他对象来进行同步(不一定是 voting):它只需要在所有线程中保持一致。

但正如 @Holger 所指出的,这违背了 ConcurrentHashMap 的目的。
为了利用 ConcurrentHashMap 的原子机制而不锁定线程,你可以使用 replace 方法,在另一个线程更改了值的情况下重试操作:

for (int i = 0; i < 16; i++) {
  new Thread(() -> {
    Integer oldValue, newValue;
    do {
       oldValue = voting.getOrDefault("GERB", 0);
       newValue = oldValue + 1; // 对值进行一些操作
    } while (!voting.replace("GERB", oldValue, newValue)); // 如果值被更改了,则重复操作
  }).start();
}
英文:

Assume there are two or more threads performs
voting.put(&quot;GERB&quot;, voting.getOrDefault(&quot;GERB&quot;, 0) + 1);

what happens?
Lets say value on key "GERB" now equals 10

  1. Thread #1 gets value voting.getOrDefault(&quot;GERB&quot;, 0). It is 10
  2. Thread #2 gets value voting.getOrDefault(&quot;GERB&quot;, 0). It is 10
  3. Thread #1 adds 1, now it is 11
  4. Thread #2 adds 1, now it is 11
  5. Thread #1 writes values 11 back to voting
  6. Thread #2 writes values 11 back to voting

Now, although 2 threads completes, the value increased only by 1 because of concurency.

So, yes, methods of ConcurrentHashMap are synchronized. That means, when one thread executes e.g. put, another thread waits. But they do not synchronize threads outside anyhow.

If you perform several calls you have to synchronize them on your own. E.g.:

final Map&lt;String, Integer&gt; voting = new ConcurrentHashMap&lt;&gt;();

for (int i = 0; i &lt; 16; i++) {
  new Thread(() -&gt; {
    synchronized (voting) { // synchronize the whole operation over the same object
       voting.put(&quot;GERB&quot;, voting.getOrDefault(&quot;GERB&quot;, 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. You have to perform that synchronization for every call to voting methods if those calls can be performed concurrently. 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.

But, as it noted by @Holger, this defeats the very purpose of the 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:

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

答案3

得分: 3

你可以将这行代码voting.put(&quot;GERB&quot;, voting.getOrDefault(&quot;GERB&quot;, 0) + 1); 分解为三个步骤:

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

现在在第1行和第3行之间,其他线程可以更改与"GERB"相关联的值,因为该方法已经返回,没有任何东西可以阻止其他线程进行更改。所以当你调用voting.put(&quot;GERB&quot;, temp)时,你覆盖了它们的值,导致它们的更新丢失。

英文:

You can divide this line voting.put(&quot;GERB&quot;, voting.getOrDefault(&quot;GERB&quot;, 0) + 1); into three steps:

int temp=voting.getOrDefault(&quot;GERB&quot;,0); //1
temp++;                                 //2
voting.put(&quot;GERB&quot;,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. So when you call voting.put(&quot;GERB&quot;,temp),you override their value which makes their update lost.

huangapple
  • 本文由 发表于 2020年10月19日 19:14:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/64426173.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定