0

Is it possible to use putIfAbsent or any of its equivalents like a short circuit operator.

myConcurrentMap.putIfAbsent(key,calculatedValue)

I want that if there is already a calculatedValue it shouldnt be calculated again. by default putIfAbsent would still do the calculation every time even though it will not actually store the value again.

Stu Thompson
  • 38,370
  • 19
  • 110
  • 156
Shikha
  • 39
  • 2
  • 10
  • This is where a closure could be an elegant solution. ;) – Peter Lawrey Jun 09 '11 at 09:20
  • You could have a `putIfAbsent(Key, Callable)` method and call it with `putIfAbsent(Key, {=> return calculation();})` – Peter Lawrey Jun 09 '11 at 10:14
  • @Peter, however `putIfAbsent` API like you suggest brings some interesting implementation issues wrt to calling same closure multiple times on different threads, locking, blocking and atomicity. – Peter Štibraný Jun 09 '11 at 10:46
  • @Peter, It would depend on the implementation, however for ConcurrentHashMap it does use locks internally, however it could result in a putIfAbsent blocking which may undesirable. – Peter Lawrey Jun 09 '11 at 10:49

3 Answers3

1

Java doesn't allow any form of short-circuiting save the built-in cases, sadly - all method calls result in the arguments being fully evaluated before control passes to the method. Thus you couldn't do this with "normal" syntax; you'd need to manually wrap up the calculation inside a Callable or similar, and then explicitly invoke it.


In this case I find it difficult to see how it could work anyway, though. putIfAbsent works on the basis of being an atomic, non-blocking operation. If it were to do what you want, the sequence of events would roughly be:

  1. Check if key exists in the map (this example assumes it doesn't)
  2. Evaluate calculatedValue (probably expensive, given the context of the question)
  3. Put result in map

It would be impossible for this to be non-blocking if the value didn't already exist at step two - two different threads calling this method at the same time could only perform correctly if blocking happened. At this point you may as well just use synchronized blocks with the flexibility of implementation that that entails; you can definitely implement what you're after with some simple locking, something like the following:

private final Map<K, V> map = ...;

public void myAdd(K key, Callable<V> valueComputation) {
    synchronized(map) {
        if (!map.containsKey(key)) {
            map.put(key, valueComputation.call());
        }
    }
}
Andrzej Doyle
  • 102,507
  • 33
  • 189
  • 228
  • ConcurrentMap isn't necessarily non-blocking. It is implemented using locks internally, although on smaller segments, not on entire map. With many writer threads, blocking is still possible though. According to documentation, "Retrieval operations (including get) generally do not block", but there is no guarantee on writing operations. – Peter Štibraný Jun 09 '11 at 09:42
  • Good point, I was thinking of the `Atomic*` classes' `compareAndSet` behaviour and expanding it to the entire map. Still, the first paragraph summarises the main question, I feel. – Andrzej Doyle Jun 09 '11 at 09:45
  • Yes I think its easiest to synchronize. Trying to do this with putIfAbsent is overkill. Also like your explanation of why this is against the basic principal. – Shikha Jun 10 '11 at 08:14
1

You can put Future<V> objects into the map. Using putIfAbsent, only one object will be there, and computation of final value will be performed by calling Future.get() (e.g. by FutureTask + Callable classes). Check out Java Concurrency in Practice for discussion about using this technique. (Example code is also in this question here on SO.

This way, your value is computed only once, and all threads get same value. Access to map isn't blocked, although access to value (through Future.get()) will block until this value is computed by one of the threads.

Community
  • 1
  • 1
Peter Štibraný
  • 32,463
  • 16
  • 90
  • 116
0

You could consider to use a Guava ComputingMap

ConcurrentMap<Key, Value> myConcurrentMap = new MapMaker()
  .makeComputingMap(
    new Function<Key, Value>() {
      public Value apply(Key key) {
        Value calculatedValue = calculateValue(key);
        return calculatedValue;
      }
  });
Sebastian Zarnekow
  • 6,609
  • 20
  • 23
  • It's an interesting idea, but it kind of violates the atomic non-blocking properties of `putIfAbsent` - in particular, calls to `get` may "block" while the object is created. As I noted in my answer, if small amounts of blocking are acceptable then the best solution is likely to just be `synchronize`-ing. – Andrzej Doyle Jun 09 '11 at 09:41