Реализация кеша с помощью java ConcurrentHashMap

Я хотел бы реализовать простое кэширование супертяжелых объектов в веб-приложении Java. Но я не могу понять, как это сделать должным образом.

Я что-то упустил или методы ConcurrentHashMap (putIfAbsent,...) недостаточно, и нужна дополнительная синхронизация?

Есть ли лучший простой API (для хранения данных, без внешней конфигурации)?

Р.

Ответы

Ответ 1

Если вам безопасно временно иметь более одного экземпляра для вещи, которую вы пытаетесь кэшировать, вы можете сделать кеш без блокировки следующим образом:

public Heavy instance(Object key) {
  Heavy info = infoMap.get(key);
  if ( info == null ) {
    // It 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;
}

Несколько потоков могут "расы" создавать и добавлять элемент для ключа, но только один должен "выиграть".

Ответ 2

В дополнение к ответу Кена, если создать тяжеловесный объект, который позже будет выброшен, НЕ приемлемо (вы хотите гарантировать, что по какой-либо причине создается только один объект для каждой клавиши), тогда вы можете сделать это с помощью... на самом деле, нет. Не делай этого сам. Используйте google-collections (теперь guava) класс MapMaker:

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

Тогда простой cache.get(key) просто работает и полностью удаляет вас от необходимости беспокоиться о сложных аспектах concurrency и synchhonization.

Обратите внимание, что если вы хотите добавить некоторые функции fancier, такие как истечение срока действия, просто

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

и вы также можете легко использовать мягкие или слабые значения для любых ключей или данных, если это необходимо (см. Javadoc для более подробной информации)

Ответ 3

Вместо того, чтобы помещать "тяжелые объекты" в кеш, вы можете использовать объекты light factory для создания активного кеша.

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 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;

Ответ 4

ConcurrentHashMap должен быть достаточным для ваших нужд putIfAbsent является потокобезопасным.

Не уверен, насколько проще вы можете получить

ConcurrentMap myCache = new ConcurrentHashMap();

Пол

Ответ 5

Я понимаю, что это старый пост, но в java 8 это можно сделать без создания потенциально неиспользуемого тяжелого объекта с помощью 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());
    }
}