Eugene
Eugene

Reputation: 120968

ConcurrentHashMap computeIfAbsent tell if first time or not

It's complicated for me to articulate a proper title for this. But an example should make it far simpler. Suppose I have this:

final class Cache {
   private static final ConcurrentHashMap<String, List<String>> CACHE = ...

   static List<String> byName(String name) {
      return CACHE.computeIfAbsent(name, x -> // some expensive operation)
   }

}

The idea is probably trivial, this acts as a LoadingCache, much like guava or caffeine (in reality it is more complicated, but that is irrelevant to the question).

I would like to be able to tell if this was the first load into the CACHE, or it was a read of an existing mapping. Currently, I do this:

final class Cache {
   private static final ConcurrentHashMap<String, List<String>> CACHE = ...

   static List<String> byName(String name) {
      boolean b[] = new boolean[1];
      List<String> result = CACHE.computeIfAbsent(name, x -> {
            b[0] = true;
            // some expensive operation)
      });

      if(b[0]) {
         // first load into the cache, do X
      } else {
         // do Y
      }

      return result;
   }

}

This works, but I am afraid I am missing something that ConcurrentHashMap can offer for me that would allow me to do the same. Thank you.

Upvotes: 8

Views: 663

Answers (2)

knittl
knittl

Reputation: 265547

If you want to avoid your single-element array to pass data out of the lambda (which I would rather do with an AtomicReference or AtomicBoolean), you could use a stateful callback object. It doesn't change the behavior or design of your code, but could be considered a little bit cleaner and more OOP-y.

class LoadingAction<K, V> {
  private boolean called = false;

  public V load(final K key) {
    called = true;
    // load data
    return ...;
  }

  public void executePostLoad() {
    if (called) {
      // loaded into cache, do X
    } else {
      // do Y
    }
  }
}

final class Cache {
   private static final ConcurrentHashMap<String, List<String>> CACHE = new ConcurrentHashMap<>();

   static List<String> byName(String name) {
      final LoadingAction<String, List<String>> loader = new LoadingAction<>();
      final List<String> result = CACHE.computeIfAbsent(name, loader::load);

      loader.executePostLoad();

      return result;
   }

}

Or turn it inside-out:

class Loader<K, V> {
  private boolean called = false;

  public V load(final Map<K, V> map, final K key) {
    final V result = map.computeIfAbsent(key, this::load);
    this.executePostLoad();
    return result;
  }

  private V load(final K key) {
    called = true;
    // load data
    return ...;
  }

  private void executePostLoad() {
    if (called) {
      // loaded into cache, do X
    } else {
      // do Y
    }
  }
}

final class Cache {
   private static final ConcurrentHashMap<String, List<String>> CACHE = new ConcurrentHashMap<>();

   static List<String> byName(String name) {
      final Loader<String, List<String>> loader = new Loader<>();
      return loader.load(CACHE, name);
   }

}

Construction and loading could be encapsulated in a static method:

class Loader<K, V> {
  private boolean called = false;

  public static <K, V> V load(final Map<K, V> map, final K key) {
      final Loader<K, V> loader = new Loader<>();
      return loader.doLoad(map, key);
  }

  private V doLoad(final Map<K, V> map, final K key) {
    final V result = map.computeIfAbsent(key, this::load);
    this.executePostLoad();
    return result;
  }

  private V load(final K key) {
    called = true;
    // load data
    return ...;
  }

  private void executePostLoad() {
    if (called) {
      // loaded into cache, do X
    } else {
      // do Y
    }
  }
}

final class Cache {
   private static final ConcurrentHashMap<String, List<String>> CACHE = new ConcurrentHashMap<>();

   static List<String> byName(String name) {
      return Loader.load(CACHE, name);
   }

}

Upvotes: 3

Stephen C
Stephen C

Reputation: 719229

I can think of a couple of ways to do this using the ConcurrentHashMap API. Both reply on using a mapping function that has side-effects. The idea is that the function makes a record of whether it was called, or what arguments it was called with.

The spec for computeIfAbsent says that the mapping function is only called if the key is absent. Alternatively, the spec for compute says that the mapping function is called with a null argument if the key is argument. In either case, if you record what happened in the mapper function via a side-effect on (say) a field of the mapper function/object, you can determine if the cache entry was already present or not.

To make this thread-safe, you need to create a fresh (thread-confined) instance of the mapper function.

Upvotes: 0

Related Questions