Does ConcurrentHashMap need synchronization when incrementing its values?

北城以北 提交于 2019-12-12 03:54:39

问题


I know ConcurrentHashMap is thread-safe e.g.putIfAbsent,Replace etc., but I was wondering, is a block of code like the one below safe?

if (accumulator.containsKey(key)) { //accumulator is a ConcurrentHashMap
    accumulator.put(key, accumulator.get(key)+1); 
} else {
    accumulator.put(key, 0); 
}

Keep in mind that the accumulator value for a key may be asked by two different threads simultaneously, which would cause a problem in a normal HashMap. So do I need something like this?

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); 
    }
}

回答1:


if (accumulator.containsKey(key)) { //accumulator is a ConcurrentHashMap
    accumulator.put(key, accumulator.get(key)+1); 
} else {
    accumulator.put(key, 0); 
}

No, this code is not thread-safe; accumulator.get(key) can be changed in between the get and the put, or the entry can be added between the containsKey and the put. If you're in Java 8, you can write accumulator.compute(key, (k, v) -> (v == null) ? 0 : v + 1), or any of the many equivalents, and it'll work. If you're not, the thing to do is write something like

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;
  }
}

...which loops until it manages to make the atomic swap. This sort of loop is pretty much how you have to do it: it's how AtomicInteger works, and what you're asking for is AtomicInteger across many keys.

Alternately, you can use a library: e.g. Guava has AtomicLongMap and ConcurrentHashMultiset, which also do things like this.




回答2:


I think the best solution for you would be to use an AtomicInteger. The nice feature here is that it is non-blocking, mutable and thread-safe. You can use the replace method offered by the CHM, but with that you will have to hold a lock of the segment/bucket-entry prior to the replace completing.

With the AtomicInteger you leverage quick non-blocking updates.

ConcurrentMap<Key, AtomicInteger> map;

then

map.get(key).incrementAndGet();

If you are using Java 8, LongAdder would be better.




回答3:


You are correct that your first code snippet is unsafe. It's totally reasonable for the thread to get interrupted right after the check has been performed and for another thread to begin executing. Therefore in the first snippet the following could happen:

[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

In this example, the behavior you would want would leave you in a state with the value for that key being set to 1, not 0.

Therefore locking is necessary.




回答4:


Only individual actions on ConcurrentHashMap are thread-safe; taking multiple actions in sequence is not. Your first block of code is not thread-safe. It is possible, for example:

THREAD A: accumulator.containsKey(key) = false
THREAD B: accumulator.containsKey(key) = false
THREAD B: accumulator.put(key, 0)
THREAD A: accumulator.put(key, 0)

Similary, it is not thread-safe to get the accumulator value for a given key, increment it, and then put it back in the map. This is a three-step process, and it is possible for another thread to interrupt at any point.

Your second synchronized block of code is thread-safe.



来源:https://stackoverflow.com/questions/35373144/does-concurrenthashmap-need-synchronization-when-incrementing-its-values

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!