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

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

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

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

P.


person Paolo1976    schedule 15.01.2010    source источник
comment
Просто интересно: каковы ваши требования к кэшированию? Вам нужно кэшировать полное транзитивное закрытие вашего тяжеловесного объекта, чтобы оно было согласованным в кластере ваших серверов приложений? Если это так, то это нетривиальная проблема, которую нужно решить, и вам может быть лучше использовать библиотеку кэширования, такую ​​​​как ehcache.   -  person Alan    schedule 15.01.2010


Ответы (5)


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

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

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

person Ken    schedule 15.01.2010
comment
Что, если вы хотите иметь метод обновления, который заменяет/обновляет тяжелый объект для данного ключа? - person Paolo1976; 15.01.2010
comment
Или просто используйте MapMaker, и только один поток когда-либо создаст Heavy. Если это нужно другому потоку, пока он еще находится в середине его создания, он просто будет ждать результата. - person Kevin Bourrillion; 16.01.2010
comment
@Paolo: Я позволю гуру MapMaker, голосующих против, ответить на этот вопрос. - person Ken; 16.01.2010

В дополнение к ответу Кена, если создание тяжеловесного объекта, который позже будет выброшен, НЕ приемлемо (по какой-то причине вы хотите гарантировать, что для каждого ключа создается только один объект), тогда вы можете сделать это... на самом деле, не. Не делай этого сам. Используйте коллекции Google (теперь гуава) класс 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) просто работает и полностью избавляет вас от необходимости беспокоиться о сложных аспектах параллелизма и синхронизации.

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

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

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

person Cowan    schedule 15.01.2010
comment
Вау, какое красивое и элегантное решение! - person Benjamin; 07.03.2012

Вместо того, чтобы помещать «тяжелые объекты» в кеш, вы можете использовать легкие фабричные объекты для создания активного кеша.

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;
person sfussenegger    schedule 15.01.2010

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

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

ConcurrentMap myCache = new ConcurrentHashMap();

Павел

person Paul Whelan    schedule 15.01.2010

Я понимаю, что это старый пост, но в 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());
    }
}
person Shane    schedule 06.10.2017