Если вы проверяете, содержит ли карта ключ, перед использованием 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);

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

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

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

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

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

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

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

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

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

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

Преимущество перед звонком 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 Collections.

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

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());
}
Другие вопросы по тегам