19

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.

Paolo1976
  • 193
  • 1
  • 1
  • 4
  • 1
    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. – Alan Jan 15 '10 at 20:37

5 Answers5

27

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)

Cowan
  • 37,227
  • 11
  • 66
  • 65
14

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".

Ken
  • 814
  • 4
  • 3
  • What if you want to have an update method that replaces/refreshes the heavy object for a given key ? – Paolo1976 Jan 15 '10 at 10:09
  • 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. – Kevin Bourrillion Jan 15 '10 at 21:34
  • @Paolo: I'll let the down-voting `MapMaker` gurus answer that. – Ken Jan 15 '10 at 21:56
  • 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 Jul 06 '22 at 02:36
  • Maybe there are use case(s) for such a thing. – z atef Jul 06 '22 at 02:36
2

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;
sfussenegger
  • 35,575
  • 15
  • 95
  • 119
1

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

Not sure how much simpler you can get

ConcurrentMap myCache = new ConcurrentHashMap();

Paul

Paul Whelan
  • 16,574
  • 12
  • 50
  • 83
0

I realize this is an old post, but in java 8 this can be done without creating a potentially unused heavy object with a ConcurrentHashMap.

public class ConcurrentCache4<K,V> {
    public static class HeavyObject
    {
    }

    private ConcurrentHashMap<String, HeavyObject> cache = new ConcurrentHashMap<>();

    public HeavyObject get(String key)
    {
        HeavyObject heavyObject = cache.get(key);
        if (heavyObject != null) {
            return heavyObject;
        }

        return cache.computeIfAbsent(key, k -> new HeavyObject());
    }
}
Shane
  • 2,271
  • 3
  • 27
  • 55