Как заполнить concurrenthashmap из нескольких потоков?

У меня есть ConcurrentHashMap, который я заполняю из нескольких потоков.

private static Map<DataCode, Long> errorMap = new ConcurrentHashMap<DataCode, Long>();

public static void addError(DataCode error) {
    if (errorMap.keySet().contains(error)) {
        errorMap.put(error, errorMap.get(error) + 1);
    } else {
        errorMap.put(error, 1L);
    }
}

Мой вышеприведенный метод addError вызывается из нескольких потоков, которые заполняют errorMap. Я не уверен, является ли это потокобезопасным? Что-то не так, что я здесь делаю?

Любое объяснение того, почему он может пропускать обновления, поможет мне лучше понять.


person john    schedule 19.06.2015    source источник


Ответы (1)


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

  1. Thread1: errorMap.get(ошибка) возвращает 1
  2. Thread2: errorMap.get(ошибка) возвращает 1
  3. Thread1: errorMap.put(ошибка, 1+1);
  4. Thread2: errorMap.put(ошибка, 1+1);

Аналогичная гонка существует вокруг операции keySet().contains(error). Чтобы исправить это, вам нужно будет использовать атомарные операции для обновления карты.

В Java 8 это легко:

errorMap.compute(error, oldValue -> oldValue == null ? 1L : oldValue + 1L);

В более старых версиях Java вам нужно использовать цикл сравнения и обновления:

Long prevValue;
boolean done;
do {
  prevValue = errorMap.get(error);
  if (prevValue == null) {
    done = errorMap.putIfAbsent(error, 1L);
  } else {
    done = errorMap.replace(error, prevValue, newValue);
  }
} while (!done);

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

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

errorAtomicLongMap.incrementAndGet(error);
person bdonlan    schedule 19.06.2015
comment
Спасибо за помощь. Теперь это имеет смысл. Если возможно, можете ли вы привести пример, как я могу использовать для этого Guava AtomicLongMap? - person john; 19.06.2015
comment
@david, добавил пример. Просто, нет? :) - person bdonlan; 19.06.2015
comment
какие? это очень просто.. Потрясающе. Кроме того, как теперь будет выглядеть объявление errorAtomicLongMap? Я заменил карту на AtomicLongMap, получил ошибку. - person john; 19.06.2015
comment
Это не совсем капля замены. Вам нужно будет объявить его просто как AtomicLongMap<DataCode>, а при чтении использовать либо метод get(error), либо метод .asMap(). - person bdonlan; 19.06.2015
comment
Я вижу, когда я пытался так, я все еще получаю сообщение об ошибке: The constructor AtomicLongMap<DataCode>() is undefined. Вот что я пытаюсь сделать: private static AtomicLongMap<DataCode> errorHolder = new AtomicLongMap<DataCode>(); Я никогда раньше этим не пользовался, поэтому сомневаюсь. - person john; 19.06.2015
comment
Используйте функцию создания: docs.guava-libraries.googlecode.com/git/javadoc/com/google/ - person bdonlan; 19.06.2015