Implementing a cache using a java ConcurrentHashMap

18,060

Solution 1

If it is safe to temporarily have more than one instance for the thing you're trying to cache, you can do a "lock-free" cache like this:

public Heavy instance(Object key) {
  Heavy info = infoMap.get(key);
  if ( info == null ) {
    // It's OK to construct a Heavy that ends up not being used
    info = new Heavy(key);
    Heavy putByOtherThreadJustNow = infoMap.putIfAbsent(key, info);
    if ( putByOtherThreadJustNow != null ) {
      // Some other thread "won"
      info = putByOtherThreadJustNow;
    }
    else {
      // This thread was the winner
    }
  }
  return info;
}

Multiple threads can "race" to create and add an item for the key, but only one should "win".

Solution 2

Further to Ken's answer, if creating a heavyweight object which later gets thrown away is NOT acceptable (you want to guarantee that only one object gets created for each key, for some reason), then you can do this by.... actually, don't. Don't do it yourself. Use the google-collections (now guava) MapMaker class:

Map<KeyType, HeavyData> cache = new MapMaker<KeyType, HeavyData>()
  .makeComputingMap(new Function<KeyType, HeavyData>() {
      public HeavyData apply(KeyType key) {
          return new HeavyData(key); // Guaranteed to be called ONCE for each key
      }
  });

Then a simple cache.get(key) just works and completely removes you from having to worry about tricky aspects of concurrency and syncrhonization.

Note that if you want to add some fancier features, like expiry, it's just

Map<....> cache = new MapMaker<....>()
  .expiration(30, TimeUnit.MINUTES)
  .makeComputingMap(.....)

and you can also easily use soft or weak values for either keys or data if required (see the Javadoc for more details)

Solution 3

Instead of putting the "heavy objects" into the cache, you could use light factory objects to create an active cache.

public abstract class LazyFactory implements Serializable {

  private Object _heavyObject;

  public getObject() {
    if (_heavyObject != null) return _heavyObject;
    synchronized {
      if (_heavyObject == null) _heavyObject = create();
    }
    return _heavyObject;
  }

  protected synchronized abstract Object create();
}

// here's some sample code

// create the factory, ignore negligible overhead for object creation
LazyFactory factory = new LazyFactory() {
  protected Object create() {
    // do heavy init here
    return new DbConnection();
  };
};
LazyFactory prev = map.pufIfAbsent("db", factory);
// use previous factory if available
return prev != null ? prev.getObject() : factory.getObject;

Solution 4

ConcurrentHashMap should be sufficient for your needs putIfAbsent is thread safe.

Not sure how much simpler you can get

ConcurrentMap myCache = new ConcurrentHashMap();

Paul

Share:
18,060

Related videos on Youtube

Paolo1976
Author by

Paolo1976

Updated on April 15, 2022

Comments

  • Paolo1976
    Paolo1976 about 2 years

    I'd like to implement a simple caching of heavyweight objects in a web java application. But I can't figure out how to do it properly.

    Am I missing something or ConcurrentHashMap methods (putIfAbsent, ...) are not enough and additional synchronization is needed ?

    Is there a better simple API (In memory storage, no external config) to do this ?

    P.

    • Alan
      Alan over 14 years
      Just wondering: what really are your requirements for caching? Do you need to cache the full transitive closure of your heavy weight object so that it is consistent across the cluster of your application servers? If so, this is a non-trivial issue to solve, and you may be better off using a cache library like ehcache.
  • Paolo1976
    Paolo1976 over 14 years
    What if you want to have an update method that replaces/refreshes the heavy object for a given key ?
  • Kevin Bourrillion
    Kevin Bourrillion over 14 years
    Or just use MapMaker, and only one thread will ever create the Heavy. If another thread needs it while it one is still in the middle of creating it, it will simply wait for the result.
  • Ken
    Ken over 14 years
    @Paolo: I'll let the down-voting MapMaker gurus answer that.
  • Benjamin
    Benjamin about 12 years
    Wow, what a nice and elegant solution!
  • z atef
    z atef almost 2 years
    That's scary, what you said. "Multiple threads can "race" to create and add an item for the key, but only one should "win"
  • z atef
    z atef almost 2 years
    Maybe there are use case(s) for such a thing.
  • z atef
    z atef almost 2 years
    I agree this is a good solution for the problem in hand, succinct yet expedient.. I was going the ThreadLocal route .