Двойная проверка блокировки с помощью ConcurrentMap

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

    private ConcurrentMap<String, Resource> map;

    // .....

    String key = "somekey";
    Resource resource;
    if (map.containsKey(key)) {
        resource = map.get(key);
    } else {
        resource = getResource(key); // I/O-bound, expensive operation
        map.put(key, resource);
    }

В приведенном выше коде несколько потоков могут проверять ConcurrentMap и видеть, что ресурса нет, и все пытаются вызвать getResource(), что дорого. Чтобы обеспечить только одну инициализацию общего ресурса и сделать код эффективным после инициализации ресурса, я хочу сделать что-то вроде этого:

    String key = "somekey";
    Resource resource;
    if (!map.containsKey(key)) {
        synchronized (map) {
            if (!map.containsKey(key)) {
                resource = getResource(key);
                map.put(key, resource);
            }
        }
    }

Это безопасная версия двойной проверки блокировки? Мне кажется, что, поскольку проверки вызывают на ConcurrentMap, он ведет себя как общий ресурс, объявленный как volatile и, таким образом, предотвращает любую из проблем "частичной инициализации", которые могут произойти.

Ответ 1

да, это безопасно.

Если map.containsKey(key) истинно, согласно doc, map.put(key, resource) происходит до него. Поэтому getResource(key) происходит до resource = map.get(key), все безопасно и звучит.

Ответ 2

Если вы можете использовать внешние библиотеки, посмотрите на Guava MapMaker.makeComputingMap(). Это сделано специально для того, что вы пытаетесь сделать.

Ответ 3

Почему бы не использовать метод putIfAbsent() на ConcurrentMap?

if(!map.containsKey(key)){
  map.putIfAbsent(key, getResource(key));
}

Возможно, вы можете вызвать getResource() более одного раза, но это не произойдет несколько раз. Упрощенный код с меньшей вероятностью укусит вас.

Ответ 4

В общем случае блокировка с двойным проверкой является безопасной, если переменная, которую вы синхронизируете, отмечена неустойчивой. Но вам лучше синхронизировать всю функцию:


public synchronized Resource getResource(String key) {
  Resource resource = map.get(key);
  if (resource == null) {
    resource = expensiveGetResourceOperation(key);    
    map.put(key, resource);
  }
  return resource;
}

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

Edit:

Это на самом деле быстрее, чем альтернативы, потому что в большинстве случаев вам не придется делать два вызова на карту. Единственной дополнительной операцией является нулевая проверка, и ее стоимость близка к нулю.

Второе редактирование:

Кроме того, вам не нужно использовать ConcurrentMap. Регулярный HashMap сделает это. Еще быстрее.

Ответ 5

Нет необходимости в этом - ConcurrentMap поддерживает это как со своим специальным атомарным putIfAbsent.

Не изобретайте велосипед: всегда используйте API там, где это возможно.

Ответ 6

Вердикт. Я приурочил 3 разных решения в точности наносекунды, так как после всего начального вопроса речь шла о производительности:

Полностью синхронизировать функцию с обычным HashMap:

synchronized (map) {

   Object result = map.get(key);
   if (result == null) {
      result = new Object();
      map.put(key, result);
   }                
   return result;
}

первый вызов: 15 000 наносекунд, последующие призывы: 700 наносекунд

Использование блокировки двойной проверки с помощью ConcurrentHashMap:

if (!map.containsKey(key)) {
   synchronized (map) {
      if (!map.containsKey(key)) {
         map.put(key, new Object());
      }
   }
} 
return map.get(key);

первый вызов: 15 000 наносекунд, последующие призывы: 1500 наносекунд

Разный аромат двойной проверки ConcurrentHashMap:

Object result = map.get(key);
if (result == null) {
   synchronized (map) {
      if (!map.containsKey(key)) {
         result = new Object();
         map.put(key, result);
      } else {
         result = map.get(key);
      }
   }
} 

return result;

первый вызов: 15 000 наносекунд, последующие призывы: 1000 наносекунд

Вы можете видеть, что наибольшая стоимость была при первом вызове, но была одинаковой для всех 3. Последующие вызовы были самыми быстрыми на обычном HashMap с использованием метода sync, например user237815, но только на 300 NANO seocnds. И ведь мы говорим о секундах NANO здесь, что означает МИЛЛИАРТ секунды.