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 获得值
voting.getOrDefault("GERB", 0)
。是10
- 线程 #2 获得值
voting.getOrDefault("GERB", 0)
。是10
- 线程 #1 加 1,现在是 11
- 线程 #2 加 1,现在是 11
- 线程 #1 将值 11 写回
voting
- 线程 #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)
时,您覆盖了它们的值,这使得它们的更新丢失。
我正在为电子选举计票,我的初始版本只有一个政党。每个选民会有不同的线程,线程将更新给定政党的票数。
我决定使用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 获得值
voting.getOrDefault("GERB", 0)
。是10 - 线程 #2 获得值
voting.getOrDefault("GERB", 0)
。是10 - 线程 #1 加 1,现在是 11
- 线程 #2 加 1,现在是 11
- 线程 #1 将值 11 写回
voting
- 线程 #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)
时,您覆盖了它们的值,这使得它们的更新丢失。