views:

926

answers:

5

Hello, 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.

+1  A: 

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
A: 

Why you call putIfAbsent? I think you shouldn't create new object before checking if the Map has it or not, otherwise the object creation will happen several times.

nanda
You are right. One should first check to see if the object is in fact available in the map. However, invoking putIfAbsent if the object currently is not is the right move.
John V.
+2  A: 

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
+2  A: 

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
What if you want to have an update method that replaces/refreshes the heavy object for a given key ?
Paolo1976
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
@Paolo: I'll let the down-voting `MapMaker` gurus answer that.
Ken
+6  A: 

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