Подтвердить что ты не робот

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

Я использую 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 таким образом?

4b9b3361

Ответ 1

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

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, содержащихся в ConcurrentMap.)

Ответ 2

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

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

Ответ 3

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

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

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

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

Ответ 4

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

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

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

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

Например:

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

Ответ 6

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

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