Если вы проверяете, содержит ли карта ключ, перед использованием putIfAbsent в ConcurrentMap - PullRequest
69 голосов
/ 20 сентября 2010

Я использую 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);

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

Как лучше всего использовать метод putIfAbsent таким образом?

Ответы [ 6 ]

105 голосов
/ 20 сентября 2010

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

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;
    }
}

(Обычный отказ от переполнения стека: с макушки головы. Не проверено. Не скомпилировано. И т.д.)

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

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

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

16 голосов
/ 21 сентября 2010

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

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

5 голосов
/ 10 июля 2014

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

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

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

Примечание. Я являюсь коммиттером для коллекций Eclipse.

3 голосов
/ 20 августа 2013

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

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.

2 голосов
/ 14 января 2016

За 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);
}
0 голосов
/ 27 мая 2014

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

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());
}
...