2

I'm trying to find a way to perform multiple operations on a ConcurrentHashMap in an atomic manner.

My logic is like this:

if (!map.contains(key)) {
    map.put(key, value);

    doSomethingElse();
}

I know there is the putIfAbsent method. But if I use it, I still won't be able to call the doSomethingElse atomically.

Is there any way of doing such things apart from resorting to synchronization / client-side locking?

If it helps, the doSomethingElse in my case would be pretty complex, involving creating and starting a thread that looks for the key that we just added to the map.

adarshr
  • 61,315
  • 23
  • 138
  • 167
  • So you want to prevent a thread from accessing the map (and seeing the new key) before `doSomethingElse` has been executed? – assylias Mar 19 '13 at 13:43
  • Yes. Because, if I let the other thread see the new key before `doSomethingElse` has been executed, it might invoke `doSomethingElse` too which would start a separate thread. – adarshr Mar 19 '13 at 13:45
  • 1
    But if you use `putIfAbsent` rather than `if !contains then put`, only one put call will ever succeed - does this resolve the contention? – Steve Townsend Mar 19 '13 at 13:46
  • @SteveTownsend I think you may have a valid point there! – adarshr Mar 19 '13 at 13:49
  • Could something like [a lock-free memoizer](http://stackoverflow.com/a/14295737/829571) pattern be used? You could maybe have a `ValueHolder` class that holds the value and the computation. And after the putIfAbsent, you could start the computation (which will do nothing if it has already been run). – assylias Mar 19 '13 at 13:50
  • Or, maybe I could call `doSomethingElse` only `if (map.contains(key))`, knowing that the `putIfAbsent` must succeed from the previous line. – adarshr Mar 19 '13 at 13:52

4 Answers4

5

If it helps, the doSomethingElse in my case would be pretty complex, involving creating and starting a thread that looks for the key that we just added to the map.

If that's the case, you would generally have to synchronize externally.

In some circumstances (depending on what doSomethingElse() expects the state of the map to be, and what the other threads might do the map), the following may also work:

if (map.putIfAbsent(key, value) == null) {
    doSomethingElse();
}

This will ensure that only one thread goes into doSomethingElse() for any given key.

NPE
  • 486,780
  • 108
  • 951
  • 1,012
  • True, but wouldn't it defeat the purpose of using a `ConcurrentHashMap` in the first place? – adarshr Mar 19 '13 at 13:46
  • Yes, but using `if !contains then put` is not idiomatic for `ConcurrentHashMap`, this may be your problem – Steve Townsend Mar 19 '13 at 13:47
  • 2
    @adarshr - it might defeat **your** purpose ... but that is because you are expecting CHM to do something that it patently does not / cannot do. – Stephen C Mar 19 '13 at 13:48
3

This would work unless you want all putting threads to wait until the first successful thread puts in the map..

if(map.get(key) == null){

  Object ret = map.putIfAbsent(key,value);
  if(ret == null){ // I won the put
     doSomethingElse();
  }
}

Now if many threads are putting with the same key only one will win and only one will doSomethingElse().

John Vint
  • 39,695
  • 7
  • 78
  • 108
  • 1
    Yes, that is exactly the behaviour I want. The `doSomethingElse` must only be executed if the put was successful, whichever thread it was. The other threads must simply fall out of the `if`. – adarshr Mar 19 '13 at 14:03
2

If your design demands that the map access and the other operation be grouped without anybody else accessing the map, then you have no choice but to lock them. Perhaps the design can be revisited to avoid this need?

This also implies that all other accesses to the map must be serialized behind the same lock.

Steve Townsend
  • 53,498
  • 9
  • 91
  • 140
2

You might keep a lock per entry. That would allow concurrent non-locking updates, unless two threads try to access the same element.

class LockedReference<T> {
  Lock lock = new ReentrantLock();;
  T value;
  LockedReference(T value) {this.value=value;}      
}

LockedReference<T> ref = new LockedReference(value);
ref.lock.lock(); //lock on the new reference, there is no contention here
try {
  if (map.putIfAbsent(key, ref)==null) {
    //we have locked on the key before inserting the element
    doSomethingElse();
   }
} finally {ref.lock.unlock();}

later

Object value;
while (true) {
   LockedReference<T> ref = map.get(key)
   if (ref!=null) {
      ref.lock.lock(); 
      //there is no contention, unless a thread is already working on this entry
      try {
         if (map.containsKey(key)) {
          value=ref.value;
          break;      
         } else {
          /*key was removed between get and lock*/
         }
      } finally {ref.lock.unlock();} 
   } else value=null;
}  

A fancier approach would be rewriting ConcurrentHashMap and have a version of putIfAbsent that accepts a Runnable (which is executed if the element was put). But that would be far far more complex.

Basically, ConcurrentHashMap implements locked segments, which is in the middle between one lock per entry, and one global lock for the whole map.

Javier
  • 12,100
  • 5
  • 46
  • 57
  • Why can't I just use `if (map.putIfAbsent(key, ref)==null) doSomethingElse();`? I think it will still be fine since only the thread that was successful in doing `putIfAbsent` can execute the `doSomethingElse` method too. In other words, makes it atomic. – adarshr Mar 19 '13 at 14:05
  • Because `doSomethingElse` would not be atomic, and another thread could `get(key)` before you have finished doing something else (i.e. it see the intermediate state immediatly after `put`). – Javier Mar 19 '13 at 14:08
  • Yes, that's the point. Another thread cannot call `doSomethingElse` unless it was the one which was successful in the `putIfAbsent(key)`. Please correct me if I am missing something. – adarshr Mar 19 '13 at 14:11
  • @adarshr - you are not missing anything – Steve Townsend Mar 19 '13 at 14:14
  • T1: `putIfAbsent("foo",1)`, succeed; T1: begin `doSomethingElse`; T2: `get("foo")` returns 1; T1: continue with `doSomethingElse`. What would happen if `doSomethingElse` does `map.put("foo",map.get("foo")+1)`? (i.e. the operation will not be atomic) – Javier Mar 19 '13 at 14:17