Paolo1976
Paolo1976

Reputation: 193

Implementing a cache using a java ConcurrentHashMap

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.

Upvotes: 19

Views: 19888

Answers (5)

Shane
Shane

Reputation: 2351

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());
    }
}

Upvotes: 0

sfussenegger
sfussenegger

Reputation: 36096

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;

Upvotes: 2

Cowan
Cowan

Reputation: 37533

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)

Upvotes: 27

Ken
Ken

Reputation: 814

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

Upvotes: 14

Paul Whelan
Paul Whelan

Reputation: 16809

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

Not sure how much simpler you can get

ConcurrentMap myCache = new ConcurrentHashMap();

Paul

Upvotes: 1

Related Questions