Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create and put a map value only if not already present, and get it: thread-safe implementation

What is the best way to make this snippet thread-safe?

private static final Map<A, B> MAP = new HashMap<A, B>();

public static B putIfNeededAndGet(A key) {
    B value = MAP.get(key);
    if (value == null) {
        value = buildB(...);
        MAP.put(key, value);
    }
    return value;
}

private static B buildB(...) {
    // business, can be quite long
}

Here are the few solutions I could think about:

  1. I could use a ConcurrentHashMap, but if I well understood, it just makes the atomic put and get operations thread-safe, i.e. it does not ensure the buildB() method to be called only once for a given value.
  2. I could use Collections.synchronizedMap(new HashMap<A, B>()), but I would have the same issue as the first point.
  3. I could set the whole putIfNeededAndGet() method synchronized, but I can have really many threads accessing this method together, so it could be quite expensive.
  4. I could use the double-checked locking pattern, but there is still the related out-of-order writes issue.

What other solutions may I have?

I know this is a quite common topic on the Web, but I didn't find a clear, full and working example yet.

like image 249
sp00m Avatar asked Nov 13 '13 09:11

sp00m


2 Answers

Use ConcurrentHashMap and the lazy init pattern which you used

public static B putIfNeededAndGet(A key) {
    B value = map.get(key);
    if (value == null) {
        value = buildB(...);
        B oldValue = map.putIfAbsent(key, value);
        if (oldValue != null) {
             value = oldValue;
        }
    }
    return value;
}
like image 146
Evgeniy Dorofeev Avatar answered Sep 29 '22 11:09

Evgeniy Dorofeev


This might not be the answer you're looking for, but use the Guava CacheBuilder, it already does all that and more:

private static final LoadingCache<A, B> CACHE = CacheBuilder.newBuilder()
   .maximumSize(100) // if necessary
   .build(
       new CacheLoader<A, B>() {
         public B load(A key) {
           return buildB(key);
         }
       });

You can also easily add timed expiration and other features as well.

This cache will ensure that load() (or in your case buildB) will not be called concurrently with the same key. If one thread is already building a B, then any other caller will just wait for that thread.

like image 30
Joachim Sauer Avatar answered Sep 29 '22 10:09

Joachim Sauer