web-dev-qa-db-ja.com

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を作成する必要はありません。この場合、どちらが良いですか?読みやすいので、私は最初のものに味方する傾向があります。 2番目の方がパフォーマンスが向上し、より正確になります。たぶん、これらのいずれかよりもこれを行うには良い方法があります。

この方法でputIfAbsentを使用するためのベストプラクティスは何ですか?

69
Chris Dail

同時実行は困難です。単純なロックの代わりに並行マップを使用する場合は、同様に行ってください。実際、必要以上にルックアップを行わないでください。

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はcomputeIfAbsentConcurrentMapデフォルトメソッドを追加しました(そしてMapは実装がConcurrentMap)。 (そして1.7が「ダイヤモンド演算子」を追加しました<>。)

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

(注:HashSetに含まれるConcurrentMapsの操作のスレッドセーフに対して責任を負います。)

105

APIの使用法がConcurrentMapの場合、トムの答えは正しいです。 putIfAbsentの使用を回避する代替方法は、GoogleCollections/Guava MapMakerのコンピューティングマップを使用することです。このマップは、指定された関数で値を自動入力し、すべてのスレッドセーフを処理します。実際には、キーごとに1つの値のみが作成され、作成関数が高価な場合、同じキーを取得することを要求する他のスレッドは、値が使用可能になるまでブロックします。

編集 Guava 11から、MapMakerは非推奨となり、Cache/LocalCache/CacheBuilderのものに置き換えられています。これは、使用方法が少し複雑ですが、基本的に同形です。

16

MutableMap.getIfAbsentPut(K, Function0<? extends V>) from Eclipse Collections (以前の GS Collections )を使用できます。

get()を呼び出し、nullチェックを行ってからputIfAbsent()を呼び出すことの利点は、キーのhashCodeを一度だけ計算し、ハッシュテーブルで正しい場所を一度だけ見つけることです。 _org.Eclipse.collections.impl.map.mutable.ConcurrentHashMap_のようなConcurrentMapsでは、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は次のように述べています。

メソッド呼び出し全体がアトミックに実行されるため、関数はキーごとに最大1回適用されます。計算の進行中に、他のスレッドがこのマップで試行した更新操作の一部がブロックされる可能性があるため、計算は短く簡単に行う必要があります...

注:私はEclipse Collectionsのコミッターです。

5
Craig P. Motlin

各スレッドの事前に初期化された値を保持することにより、受け入れられた答えを改善できます。

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

最近、SetではなくAtomicIntegerマップ値でこれを使用しました。

3
karmakaze

5年以上後、この問題を解決するためにThreadLocalを使用するソリューションについて誰も言及または投稿していないとは信じられません。このページのソリューションのいくつかはスレッドセーフではありませんであり、ずさんです。

この特定の問題にThreadLocalsを使用することは、並行性のためにベストプラクティスだけでなく、ゴミ/オブジェクトの作成を最小限に抑えるためにも考慮されますduringスレッドの競合。また、信じられないほどクリーンなコードです。

例えば:

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

私の一般的な近似:

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