Двойная проверка блокировки с ConcurrentMap - PullRequest
11 голосов
/ 10 августа 2011

У меня есть фрагмент кода, который может быть выполнен несколькими потоками, который должен выполнить операцию ввода-вывода для инициализации общего ресурса, который хранится в 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, и, таким образом, предотвращает любые проблемы "частичной инициализации", которые могут возникнуть.

Ответы [ 6 ]

4 голосов
/ 10 августа 2011

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

3 голосов
/ 10 августа 2011

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

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

2 голосов
/ 10 августа 2011

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

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

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

1 голос
/ 10 августа 2011

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


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

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

Редактировать:

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

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

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

0 голосов
/ 17 августа 2012

Приговор вынесен. Я рассчитал 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 с синхронизацией методов, как предложено user237815, но только с 300 NANO-секундами.И в конце концов, мы говорим здесь о NANO секундах, что означает МИЛЛИАРД секунды.

0 голосов
/ 10 августа 2011

В этом нет необходимости - ConcurrentMap поддерживает это, как и с помощью специального атомарного putIfAbsent метода.

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

...