Если вы проверяете, содержит ли карта containsKey перед использованием Putifabsent ConcurrentMap


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

ConcurrentMap<String, Set<X>> map = new ConcurrentHashMap<String, Set<X>>();

// ...

map.putIfAbsent(name, new HashSet<X>());
map.get(name).add(Y);

удобочитаемость это здорово, но это требует создания нового HashSet каждый раз, даже если он уже находится на карте. Я мог бы написать так:

if (!map.containsKey(name)) {
    map.putIfAbsent(name, new HashSet<X>());
}
map.get(name).add(Y);

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

какова наилучшая практика использования putIfAbsent таким образом?

6 68

6 ответов:

параллелизм сложно. Если вы собираетесь беспокоиться о параллельных картах вместо простой блокировки, вы можете также пойти на это. Действительно, не делайте поисков больше, чем необходимо.

Set<X> set = map.get(name);
if (set == null) {
    final Set<X> value = new HashSet<X>();
    set = map.putIfAbsent(name, value);
    if (set == null) {
        set = value;
    }
}

(обычный StackOverflow отказ от ответственности: с верхней части моей головы. Не тестировать. Не компилировать. Так далее.)

обновление: 1.8 добавил computeIfAbsent метода по умолчанию для ConcurrentMapMap что довольно интересно, потому что эта реализация была бы неправильной для ConcurrentMap). (И 1.7 добавил "алмазный оператор"<>.)

Set<X> set = map.computeIfAbsent(name, n -> new HashSet<>());

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

ответ Тома верен в том, что касается использования API для ConcurrentMap. Альтернативой, которая позволяет избежать использования putIfAbsent, является использование вычислительной карты из GoogleCollections/Guava MapMaker, которая автоматически заполняет значения поставляемой функцией и обрабатывает всю потокобезопасность для вас. На самом деле он создает только одно значение для каждого ключа, и если функция create стоит дорого, другие потоки, запрашивающие получение того же ключа, будут блокироваться до тех пор, пока значение не станет доступный.

Edit из Guava 11 MapMaker устарел и заменяется материалом Cache/LocalCache/CacheBuilder. Это немного сложнее в использовании, но в основном изоморфны.

можно использовать MutableMap.getIfAbsentPut(K, Function0<? extends V>) С Коллекции Eclipse (ранее коллекции GS).

преимущество над вызовом get(), делая нулевую проверку, а затем вызывая putIfAbsent() это то, что мы будем только вычислять хэш-код ключа один раз, и найти нужное место в хэш-таблице один раз. В ConcurrentMaps, как org.eclipse.collections.impl.map.mutable.ConcurrentHashMap реализация getIfAbsentPut() также потокобезопасен и атомарен.

import org.eclipse.collections.impl.map.mutable.ConcurrentHashMap;
...
ConcurrentHashMap<String, MyObject> map = new ConcurrentHashMap<>();
map.getIfAbsentPut("key", () -> someExpensiveComputation());

реализация org.eclipse.collections.impl.map.mutable.ConcurrentHashMap is действительно не блокирует. Хотя предпринимаются все усилия, чтобы не вызывать заводскую функцию без необходимости, все еще есть шанс, что она будет вызвана более одного раза во время конфликта.

этот факт отличает его от Java 8-х ConcurrentHashMap.computeIfAbsent(K, Function<? super K,? extends V>). Javadoc для этого метода гласит:

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

Примечание: я коммиттер для коллекций Eclipse.

сохраняя предварительно инициализированное значение для каждого потока, вы можете улучшить принятый ответ:

Set<X> initial = new HashSet<X>();
...
Set<X> set = map.putIfAbsent(name, initial);
if (set == null) {
    set = initial;
    initial = new HashSet<X>();
}
set.add(Y);

Я недавно использовал это со значениями карты AtomicInteger, а не Set.

через 5 лет я не могу поверить, что никто не упомянул или не опубликовал решение, которое использует ThreadLocal чтобы решить эту проблему; и несколько решений на эту страницу не ориентирована на многопотоковое исполнение и просто неаккуратно.

использование ThreadLocals для этой конкретной проблемы не только рассматривается лучшие практики для параллелизма, но для минимизации мусора/создание объекта во время нить раздора. Кроме того, это невероятно чистый код.

например:

private final ThreadLocal<HashSet<X>> 
  threadCache = new ThreadLocal<HashSet<X>>() {
      @Override
      protected
      HashSet<X> initialValue() {
          return new HashSet<X>();
      }
  };


private final ConcurrentMap<String, Set<X>> 
  map = new ConcurrentHashMap<String, Set<X>>();

и собственно логика...

// minimize object creation during thread contention
final Set<X> cached = threadCache.get();

Set<X> data = map.putIfAbsent("foo", cached);
if (data == null) {
    // reset the cached value in the ThreadLocal
    listCache.set(new HashSet<X>());
    data = cached;
}

// make sure that the access to the set is thread safe
synchronized(data) {
    data.add(object);
}

мое общее приближение:

public class ConcurrentHashMapWithInit<K, V> extends ConcurrentHashMap<K, V> {
  private static final long serialVersionUID = 42L;

  public V initIfAbsent(final K key) {
    V value = get(key);
    if (value == null) {
      value = initialValue();
      final V x = putIfAbsent(key, value);
      value = (x != null) ? x : value;
    }
    return value;
  }

  protected V initialValue() {
    return null;
  }
}

и в качестве примера использования:

public static void main(final String[] args) throws Throwable {
  ConcurrentHashMapWithInit<String, HashSet<String>> map = 
        new ConcurrentHashMapWithInit<String, HashSet<String>>() {
    private static final long serialVersionUID = 42L;

    @Override
    protected HashSet<String> initialValue() {
      return new HashSet<String>();
    }
  };
  map.initIfAbsent("s1").add("chao");
  map.initIfAbsent("s2").add("bye");
  System.out.println(map.toString());
}