ConcurrentHashMap 在递增其值时是否需要同步?
Does ConcurrentHashMap need synchronization when incrementing its values?
我知道 ConcurrentHashMap 是线程安全的 e.g.putIfAbsent,Replace 等,但我想知道,像下面这样的代码块安全吗?
if (accumulator.containsKey(key)) { //accumulator is a ConcurrentHashMap
accumulator.put(key, accumulator.get(key)+1);
} else {
accumulator.put(key, 0);
}
请记住,一个键的累加器值可能会同时被两个不同的线程询问,这会导致普通 HashMap 出现问题。那么我需要这样的东西吗?
ConcurrentHashMap<Integer,Object> locks;
...
locks.putIfAbsent(key,new Object());
synchronized(locks.get(key)) {
if (accumulator.containsKey(key)) {
accumulator.put(key, accumulator.get(key)+1);
} else {
accumulator.put(key, 0);
}
}
您的第一个代码片段不安全是正确的。执行完检查后立即中断线程并让另一个线程开始执行是完全合理的。因此在第一个片段中可能会发生以下情况:
[Thread 1]: Check for key, return false
[Thread 2]: Check for key, return false
[Thread 2]: Put value 0 in for key
[Thread 1]: Put value 0 in for key
在此示例中,您想要的行为将使您处于该键的值设置为 1 而不是 0 的状态。
因此需要锁定。
if (accumulator.containsKey(key)) { //accumulator is a ConcurrentHashMap
accumulator.put(key, accumulator.get(key)+1);
} else {
accumulator.put(key, 0);
}
不,这段代码不是线程安全的; accumulator.get(key)
可以在 get
和 put
之间更改,或者可以在 containsKey
和 put
之间添加条目。如果你在 Java 8,你可以写 accumulator.compute(key, (k, v) -> (v == null) ? 0 : v + 1)
,或者许多等价物中的任何一个,它会起作用。如果你不是,要做的就是写一些像
这样的东西
while (true) {
Integer old = accumulator.get(key);
if (old == null) {
if (accumulator.putIfAbsent(key, 0) == null) {
// note: it's a little surprising that you want to put 0 in this case,
// are you sure you don't mean 1?
break;
}
} else if (accumulator.replace(key, old, old + 1)) {
break;
}
}
...循环直到它设法进行原子交换。这种循环几乎就是您 执行此操作的方式:这就是 AtomicInteger
的工作方式,而您所要求的是 AtomicInteger
跨多个键。
或者,您可以使用库:例如Guava 有 AtomicLongMap
和 ConcurrentHashMultiset
,它们也做这样的事情。
只有 ConcurrentHashMap 上的单个操作是线程安全的;按顺序执行多个操作不是。您的第一段代码不是线程安全的。可以,例如:
THREAD A: accumulator.containsKey(key) = false
THREAD B: accumulator.containsKey(key) = false
THREAD B: accumulator.put(key, 0)
THREAD A: accumulator.put(key, 0)
同样,获取给定键的累加器值、递增它,然后将其放回映射中并不是线程安全的。这是一个三步过程,另一个线程有可能在任何时候中断。
您的第二个同步代码块是线程安全的。
我认为最适合您的解决方案是使用 AtomicInteger
。这里的好处是它是非阻塞的、可变的和线程安全的。您可以使用 CHM 提供的 replace
方法,但是在替换完成之前,您必须持有 segment/bucket-entry 的锁。
借助 AtomicInteger
,您可以利用快速非阻塞更新。
ConcurrentMap<Key, AtomicInteger> map;
然后
map.get(key).incrementAndGet();
如果你用的是Java8,LongAdder
会更好。
我知道 ConcurrentHashMap 是线程安全的 e.g.putIfAbsent,Replace 等,但我想知道,像下面这样的代码块安全吗?
if (accumulator.containsKey(key)) { //accumulator is a ConcurrentHashMap
accumulator.put(key, accumulator.get(key)+1);
} else {
accumulator.put(key, 0);
}
请记住,一个键的累加器值可能会同时被两个不同的线程询问,这会导致普通 HashMap 出现问题。那么我需要这样的东西吗?
ConcurrentHashMap<Integer,Object> locks;
...
locks.putIfAbsent(key,new Object());
synchronized(locks.get(key)) {
if (accumulator.containsKey(key)) {
accumulator.put(key, accumulator.get(key)+1);
} else {
accumulator.put(key, 0);
}
}
您的第一个代码片段不安全是正确的。执行完检查后立即中断线程并让另一个线程开始执行是完全合理的。因此在第一个片段中可能会发生以下情况:
[Thread 1]: Check for key, return false
[Thread 2]: Check for key, return false
[Thread 2]: Put value 0 in for key
[Thread 1]: Put value 0 in for key
在此示例中,您想要的行为将使您处于该键的值设置为 1 而不是 0 的状态。
因此需要锁定。
if (accumulator.containsKey(key)) { //accumulator is a ConcurrentHashMap
accumulator.put(key, accumulator.get(key)+1);
} else {
accumulator.put(key, 0);
}
不,这段代码不是线程安全的; accumulator.get(key)
可以在 get
和 put
之间更改,或者可以在 containsKey
和 put
之间添加条目。如果你在 Java 8,你可以写 accumulator.compute(key, (k, v) -> (v == null) ? 0 : v + 1)
,或者许多等价物中的任何一个,它会起作用。如果你不是,要做的就是写一些像
while (true) {
Integer old = accumulator.get(key);
if (old == null) {
if (accumulator.putIfAbsent(key, 0) == null) {
// note: it's a little surprising that you want to put 0 in this case,
// are you sure you don't mean 1?
break;
}
} else if (accumulator.replace(key, old, old + 1)) {
break;
}
}
...循环直到它设法进行原子交换。这种循环几乎就是您 执行此操作的方式:这就是 AtomicInteger
的工作方式,而您所要求的是 AtomicInteger
跨多个键。
或者,您可以使用库:例如Guava 有 AtomicLongMap
和 ConcurrentHashMultiset
,它们也做这样的事情。
只有 ConcurrentHashMap 上的单个操作是线程安全的;按顺序执行多个操作不是。您的第一段代码不是线程安全的。可以,例如:
THREAD A: accumulator.containsKey(key) = false
THREAD B: accumulator.containsKey(key) = false
THREAD B: accumulator.put(key, 0)
THREAD A: accumulator.put(key, 0)
同样,获取给定键的累加器值、递增它,然后将其放回映射中并不是线程安全的。这是一个三步过程,另一个线程有可能在任何时候中断。
您的第二个同步代码块是线程安全的。
我认为最适合您的解决方案是使用 AtomicInteger
。这里的好处是它是非阻塞的、可变的和线程安全的。您可以使用 CHM 提供的 replace
方法,但是在替换完成之前,您必须持有 segment/bucket-entry 的锁。
借助 AtomicInteger
,您可以利用快速非阻塞更新。
ConcurrentMap<Key, AtomicInteger> map;
然后
map.get(key).incrementAndGet();
如果你用的是Java8,LongAdder
会更好。