Реализация параметризованного потокобезопасного кеша с отложенной инициализацией с использованием ConcurrentHashMap (без ключевого слова volatile)

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

Идиома одиночной проверки

private volatile FieldType field;
FieldType getField() {
  FieldType result = field;
  if (result == null) {
    field = result = computeFieldValue();
  }
  return result;
}

Здесь нам нужен volatile field, чтобы избежать передачи частично инициализированного объекта в другой поток, то есть — присваивание (запись) неявно выполняет необходимую синхронизацию.

Я хотел бы реализовать параметризованный кеш отложенной инициализации, который по сути представлен Map<Integer, Object>, где каждый элемент создается с использованием отложенной инициализации.

Мой вопрос: достаточно ли использовать ConcurrentHashMap, чтобы избежать проблемы с частичной инициализацией. То есть в этом случае поточно-безопасная реализация кеша с отложенной инициализацией с использованием идиомы одиночной проверки может быть задана следующим образом:

private final ConcurrentHashMap<Integer, ItemType> items = new ConcurrentHashMap<Integer, ItemType>();

ItemType getItem(Integer index) {
  ItemType result = items.get(index);
  if (result == null) {
    result = computeItemValue(index);
    items.put(index, result);
  }
  return result;
}

Другими словами: я предполагаю, что «items.put(index, result)» выполняет необходимую синхронизацию (поскольку это запись). Обратите внимание, что вопрос здесь может быть двояким: во-первых, мне интересно, работает ли это в (а) текущей реализации JVM, во-вторых (что более важно), интересно, гарантируется ли это (с учетом документации/контракта) ConcurrentHashMap.

Примечание. Здесь я предполагаю, что calculateItemValue генерирует неизменяемый объект и гарантирует безопасность потока в смысле идиомы одиночной проверки (то есть, после завершения построения объекта возвращаемый объект ведет себя одинаково для всех потоков). Я предполагаю, что это пункт 71 в книге Дж. Блоха.


person Christian Fries    schedule 11.08.2015    source источник
comment
Ни одно из ваших предложений не является потокобезопасным. Несколько потоков могут вызывать computeFieldValue и присваивать значение полю/добавлять его на карту. Для ConcurrentHashMap используйте putIfAbsent или computeIfAbsent.   -  person Sotirios Delimanolis    schedule 11.08.2015
comment
Они потокобезопасны в смысле идиомы с одиночной проверкой, когда calculateFieldValue создает неизменяемый объект, который ведет себя одинаково, даже если создается несколько раз... (пожалуйста, идиома с одиночной проверкой в ​​книге Блоха).   -  person Christian Fries    schedule 11.08.2015
comment
@ChristianFries Правильный одноэлементный шаблон не инициализирует экземпляр более одного раза.   -  person Kayaman    schedule 11.08.2015
comment
@Kayaman: В этом случае мне нужно было бы использовать идиому двойной проверки. Я знаю это, но это связано с дополнительной синхронизацией и с точки зрения производительности, возможно, лучше использовать идиому с одной проверкой.   -  person Christian Fries    schedule 11.08.2015


Ответы (2)


В java 8 вы можете использовать функцию computeIfAbsent, чтобы избежать даже возможности дублирования инициализации:

private final ConcurrentHashMap<Integer, ItemType> items = new ConcurrentHashMap<Integer, ItemType>();

ItemType getItem(Integer index) {
  return items.computeIfAbsent(index, this::computeItemValue);
}
person Brett Okken    schedule 11.08.2015

достаточно ли использовать ConcurrentHashMap, чтобы избежать проблемы с частичной инициализацией.

Да, есть явный случай — перед упорядочением объектов, прочитанных из CHM, по сравнению с записью.

Итак, вы затронули видимость, но не затронули атомарность. Есть два компонента атомарности

  • запоминание
  • поставить, если нет

Вы тоже не достигаете. То есть для мемоизации можно создать более одного объекта (не собственно lazy-init). А для put, если он отсутствует, CHM может получить более одного значения, поскольку вы не вызываете putIfAbsent.

Основываясь на вашем последнем обновлении, если они будут одним и тем же объектом и неизменяемы, все должно быть в порядке, несмотря на дополнительную конструкцию.

person John Vint    schedule 11.08.2015
comment
Спасибо. Что, если у нас есть ситуация, когда у нас есть два разных неизменяемых объекта, которые ведут себя неразличимо (за исключением того, что у них могут быть два разных адреса/идентификатора)? (Я отредактирую свой вопрос в этом направлении) - person Christian Fries; 11.08.2015
comment
@ChristianFries Что вы имеете в виду под идентификатором? Как последовательность? В таком случае вы в беде. Если вы можете создать два объекта ItemType a = computeItemValue(index) и ItemType b = computeItemValue(index) с одним и тем же index так, что a.equals(b) будет ложным. Это не сработает. - person John Vint; 11.08.2015
comment
Если вы используете Java 8 и можете использовать computeIfAbsent, как предложил Бретт Оккен, то обязательно сделайте это. - person John Vint; 11.08.2015
comment
Под id я имел в виду s.th. связаны с адресом памяти объекта, но объекты считаются равными в смысле .equals. - person Christian Fries; 13.08.2015