12

に格納されている共有リソースを初期化するためにI/Oバウンド操作を実行する必要がある複数のスレッドで実行できるコードがありますConcurrentMap。このコードスレッドを安全にし、共有リソースを初期化するための不要な呼び出しを回避する必要があります。バグのあるコードは次のとおりです。

    private ConcurrentMap<String, Resource> map;

    // .....

    String key = "somekey";
    Resource resource;
    if (map.containsKey(key)) {
        resource = map.get(key);
    } else {
        resource = getResource(key); // I/O-bound, expensive operation
        map.put(key, resource);
    }

上記のコードを使用すると、複数のスレッドがをチェックしてリソースが存在しないことを確認し、すべてが高額なConcurrentMap呼び出しを試みる可能性があります。getResource()共有リソースの単一の初期化のみを保証し、リソースが初期化された後にコードを効率的にするために、私は次のようなことをしたいと思います。

    String key = "somekey";
    Resource resource;
    if (!map.containsKey(key)) {
        synchronized (map) {
            if (!map.containsKey(key)) {
                resource = getResource(key);
                map.put(key, resource);
            }
        }
    }

これはダブルチェックロックの安全なバージョンですか?チェックが呼び出されるので、チェックConcurrentMapは宣言された共有リソースのように動作し、volatile発生する可能性のある「部分的な初期化」の問題を防ぐように思われます。

4

6 に答える 6

4

外部ライブラリを使用できる場合は、GuavaのMapMaker.makeComputingMap()をご覧ください。それはあなたがやろうとしていることに合わせて作られています。

于 2011-08-09T22:00:49.483 に答える
3

はい、安全です。

map.containsKey(key)ドキュメントによると、trueの場合、そのmap.put(key, resource)前に発生します。したがって、getResource(key)前に起こりresource = map.get(key)ます、すべてが安全で健全です。

于 2011-08-09T21:49:14.280 に答える
2

ConcurrentMapでputIfAbsent()メソッドを使用してみませんか?

if(!map.containsKey(key)){
  map.putIfAbsent(key, getResource(key));
}

getResource()を複数回呼び出すことも考えられますが、何度も呼び出されることはありません。単純なコードはあなたを噛む可能性が低くなります。

于 2011-08-09T21:32:30.053 に答える
1

一般に、同期している変数が揮発性としてマークされている場合、ダブルチェックロックは安全です。ただし、関数全体を同期することをお勧めします。


public synchronized Resource getResource(String key) {
  Resource resource = map.get(key);
  if (resource == null) {
    resource = expensiveGetResourceOperation(key);    
    map.put(key, resource);
  }
  return resource;
}

パフォーマンスへの影響はわずかであり、同期の問題が発生しないことを確認できます。

編集:

ほとんどの場合、マップに対して2回の呼び出しを行う必要がないため、これは実際には他の方法よりも高速です。唯一の追加操作はヌルチェックであり、そのコストはゼロに近いです。

2番目の編集:

また、ConcurrentMapを使用する必要はありません。通常のHashMapがそれを行います。さらに速く。

于 2011-08-09T22:03:36.193 に答える
0

その必要はありません-ConcurrentMapは、特別なアトミックputIfAbsentメソッドと同様にこれをサポートします

車輪の再発明をしないでください。可能な場合は常にAPIを使用してください。

于 2011-08-09T21:33:55.687 に答える
0

評決が出ました。最初の質問はパフォーマンスに関するものだったので、ナノ秒の精度で3つの異なるソリューションのタイミングを計りました。

通常のHashMapで関数を完全に同期します

synchronized (map) {

   Object result = map.get(key);
   if (result == null) {
      result = new Object();
      map.put(key, result);
   }                
   return result;
}

最初の呼び出し:15,000ナノ秒、その後の呼び出し:700ナノ秒

ConcurrentHashMapでダブルチェックロックを使用する

if (!map.containsKey(key)) {
   synchronized (map) {
      if (!map.containsKey(key)) {
         map.put(key, new Object());
      }
   }
} 
return map.get(key);

最初の呼び出し:15,000ナノ秒、その後の呼び出し:1500ナノ秒

ダブルチェックされたConcurrentHashMapの別のフレーバー

Object result = map.get(key);
if (result == null) {
   synchronized (map) {
      if (!map.containsKey(key)) {
         result = new Object();
         map.put(key, result);
      } else {
         result = map.get(key);
      }
   }
} 

return result;

最初の呼び出し:15,000ナノ秒、その後の呼び出し:1000ナノ秒

最大のコストは最初の呼び出しであったことがわかりますが、3つすべてで同様でした。後続の呼び出しは、user237815のようなメソッド同期を使用した通常のHashMapで最速でしたが、300NANOの秒数しかありませんでした。そして結局のところ、ここではナノ秒について話しているのです。これは10億秒を意味します。

于 2012-08-17T15:37:46.903 に答える