2010-09-20 2 views
67

Я использую ConcurrentMap Java для карты, которая может использоваться из нескольких потоков. PutIfAbsent - отличный метод и гораздо легче читать/писать, чем использовать стандартные операции с картами. У меня есть некоторый код, который выглядит следующим образом:Если вы проверяете, содержит ли mapKey перед использованием ConcurrentMap's 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 таким образом?

+3

В вашем примере, Value-HashSet должен был бы быть ConcurrentHashSet тоже, иначе это еще не поточно. –

+1

Решение Tom Hawtin - это именно то, что вы ищете –

+1

Как указал Маркус, тип значения (набор в этом случае) также должен быть потокобезопасным, так как к нему могут одновременно обращаться несколько потоков. – sjlee

ответ

99

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

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 отказ от ответственности:. С верхней части моей головы Не тестировалось Не компилируется Etc..).

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

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

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

+22

+1 для «параллелизма жестко» и с использованием возвращаемого значения putIfAbsent –

+1

@Markus - +1 вам, а также для указания очевидного, но легко проигнорировать факт, что повторное использование возвращаемого значения является хорошей практикой. –

+1

Хороший ответ. Напоминает мне о двойной проверке блокировки: http: //en.wikipedia.org/wiki/Double-checked_locking –

16

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

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

+0

Я просто попробовал это, и это отличное решение. Вы получаете все преимущества ConcurrentMap, не беспокоясь о идионах putIfAbsent, которые легко испортить. –

3

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

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, а не набор.

+0

Как отмечено в обновлении принятого ответа, Java 1.8 добавляет computeIfAbsent, который достигает того же результата и намного проще. – karmakaze

+0

Контекст, который должен использоваться в этом коде, будет сложным. Это вызовет «интересный» поиск ошибок по дороге, если не сразу. Я не уверен, что это победа в производительности. (Также вам нужно заблокировать доступ к 'HashSet'.) –

0

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

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

Вы можете использовать MutableMap.getIfAbsentPut(K, Function0<? extends V>) из Eclipse Collections (ранее 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's ConcurrentHashMap.computeIfAbsent(K, Function<? super K,? extends V>). Javadoc для этого метода состояний:

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

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

+3

Мне это очень нравится. Я задал этот вопрос много лет назад, но с Java 8 это действительно приятное решение. –

2

В 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); 
} 
+1

Но OP хотел иметь нит-общий набор, не так ли? – Antoniossss

+0

Это «общий поток». ThreadLocal предназначен для предотвращения ненужного создания объекта (Map и, следовательно, Set create is cheap) во время вызова putIfAbsent(). Набор правильно и безопасно опубликован для всех потоков. – Nathan

+0

'threadCache' возвращает другой набор для каждого потока. – Antoniossss

Смежные вопросы