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) 可以在 getput 之间更改,或者可以在 containsKeyput 之间添加条目。如果你在 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 有 AtomicLongMapConcurrentHashMultiset,它们也做这样的事情。

只有 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会更好。