74

複数のスレッドから使​​用できるマップに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を使用するためのベストプラクティスは何ですか?

4

6 に答える 6

108

並行性は難しいです。単純なロックではなく同時マップを気にする場合は、それを選択することをお勧めします。実際、必要以上にルックアップを行わないでください。

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

于 2010-09-20T14:11:15.817 に答える
16

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

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

于 2010-09-21T02:23:34.847 に答える
5

Eclipseコレクション(以前のGSコレクションMutableMap.getIfAbsentPut(K, Function0<? extends V>))から使用できます。

get()を呼び出し、nullチェックを実行してから呼び出すことの利点putIfAbsent()は、キーのhashCodeを1回だけ計算し、ハッシュテーブル内の適切な場所を1回見つけることです。のようなConcurrentMapsorg.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は本当にノンブロッキングです。ファクトリ関数を不必要に呼び出さないようにあらゆる努力が払われていますが、それでも競合中に複数回呼び出される可能性があります。

この事実は、Java8とは一線を画していますConcurrentHashMap.computeIfAbsent(K, Function<? super K,? extends V>)。このメソッドのJavadocには、次のように記載されています。

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

注:私はEclipseコレクションのコミッターです。

于 2014-07-10T17:31:08.157 に答える
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);

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

于 2013-08-19T20:05:07.810 に答える
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);
}
于 2016-01-14T00:50:53.277 に答える
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());
}
于 2014-05-26T20:49:11.137 に答える