4

オブジェクトを作成するこのシングルトンがあります。現在、次のようになっています。

public ApplicationManagerSingleton {
    ....
    private Map<String, Thing> map = new HashMap<String, Thing>();

    public Thing getThingById( String id ) {
       Thing t = null;
       if ( !map.contains(id) ) {
        t = longAndCostlyInitializationOfThing();
        map.put(id, t );
       }
       return map.get(id);
     }
 }

明らかな問題は、2 つのスレッドが同じものにアクセスしようとすると、それらが重複してしまう可能性があることです。

だから私はロックを使用しました:

 public ApplicationManagerSingleton {
      private Map<String, Thing> map = new HashMap<Sring, Thing>();
      public Thing getThingById(String id ) {
          synchronized( map ) {
             if (!map.contains(id)) {
                 t = initialize....
             }
             map.put(id, t);
           }
           returns map.get(id);
      }
 }

しかし、これは最悪です。新しいリソースが作成されるたびにしばらくの間マップをロックすることになるため、他のスレッドが別のことを望んでいるというデメリットがあります。

Java 5 の並行パッケージを使えばもっと良くなると確信しています。誰かが私を正しい方向に向けることができますか?

私が避けたいのは、他のことに関心のある他のスレッドのクラスまたはマップをロックすることです。

4

6 に答える 6

3

アイテムを複数回作成してブロックをできるだけ少なくしたい場合は、2 つのマップを使用します。1 つはオブジェクトの作成時に使用する一連のロックを保持するためのもので、もう 1 つはオブジェクトを保持するためのものです。

次のようなことを考えてみましょう:

private ConcurrentMap<String, Object> locks = new ConcurrentHashMap<String, Object>();
private ConcurrentMap<String, Thing> things = new ConcurrentHashMap<String, Thing>();

public void Thing getThingById(String id){
    if(!things.containsKey(id)){        
      locks.putIfAbsent(id, new Object());
      synchronized(locks.get(id)){
         if (!things.containsKey(id)){
             things.put(id, createThing());
         }
      }
    }

    return things.get(id);
}

これにより、複数のスレッドが同じキーを取得しようとしているのをブロックするだけで、同じキーThingが 2 回作成されるのを防ぐことができます。

アップデート

Guava Cache の例は、新しい Answer に移動しました。

于 2013-01-04T18:15:57.543 に答える
3

多分 ConcurrentHashMap があなたを助けることができます。その名前が示すように、同時変更をサポートします。

新しい要素を 1 回だけ作成するには、次のようにします。

private Map<String,Thing> map = new ConcurrentHashMap<>();
private final Object lock = new Object();
public Thing getById(String id) {
  Thing t = map.get(id);
  if (t == null) {
    synchronized(lock) {
      if (!map.containsKey(id)) {
        t = //create t
        map.put(id, t);
      }
    }
  }
  return t;
}

一度に新しいものを作成できるのは 1 つのスレッドだけですが、既存の値についてはロックはまったくありません。

ロックを完全に回避したい場合は、2 つのマップを使用する必要がありますが、やや複雑になり、多くのスレッドがマップに継続的に入力されることが本当に予想される場合にのみ価値があります。その場合、FutureTasks をスレッド プールと一緒に使用してオブジェクトを非同期的に作成し、ロックがかかる時間を最小限に抑える方がよい場合があります (1 つのスレッドだけが新しい要素を作成するようにロックが必要です)。

コードは次のようになります。

private Map<String,Future<Thing>> map = new ConcurrentHashMap<>();
private final Object lock = new Object();
ExecutorService threadPool = ...;
public Thing getById(String id) {
  Future<Thing> t = map.get(id);
  if (t == null) {
    synchronized(lock) {
      if (!map.containsKey(id)) {
        Callable<Thing> c = //create a Callable that creates the Thing
        t = threadPool.submit(c);
        map.put(id, t);
      }
    }
  }
  return t.get();
}

ロックは、Callable を作成し、それをスレッド プールに送信して Future を取得し、その Future をマップに配置するのにかかる時間だけ有効になります。Callable はスレッド プールに要素を作成し、要素を返すと、Future の get() メソッドがロックを解除してその値を返します (待機中のスレッドの場合、後続の呼び出しはロックされません)。

于 2013-01-04T18:32:14.157 に答える
2

それを調べた後、私Guava's LoadingCacheはおそらくこれに対する非常に良い解決策だと思います。デフォルトでは、CacheBuilderはエビクションを行わない(つまり、単なるマップである)キャッシュを作成し、すでに組み込まれているキーのロード時にスレッドをブロックします。

LoadingCache

CacheBuilder

 private Cache<String, Thing> myCache;

 MyConstructor(){
    myCache = CacheBuilder.newBuilder().build(
       new CacheLoader<String, Thing>() {
         public Thing load(String key) throws AnyException {
           return createExpensiveGraph(key);
         }
        });
 }

  public void Thing getThingById(String id){
    return myCache.get(id);
  }
于 2013-01-04T20:27:50.420 に答える
1

を使用できますConcurrentHashMap。これは、複数のスレッドから問題なくアクセスできます。

于 2013-01-04T18:12:44.557 に答える
1

マップに軽量プロキシをすぐに挿入できます。プロキシは、初期化されると実際のオブジェクトに委任しますが、それまでブロックします。リアルThingが初期化されると、マップ内のプロキシを置き換えることができます。

private Map<String, Thing> map = new ConcurrentHashMap<>();

public Thing getThingById(String id) {
    ThingProxy proxy = null;
    synchronized (map) {
        if (!map.containsKey(id)) {
            proxy = new ThingProxy();
            map.put(id, proxy);
        }
    }
    if (proxy != null) {
        proxy.initialize();
        map.put(id, proxy.getRealThing());
    }
    return map.get(id);
}

private class ThingProxy implements Thing {

    private Thing realThing;
    private CountDownLatch countDownLatch = new CountDownLatch(1);

    @Override
    public void someMethodOfThing() {
        try {
            countDownLatch.await();
            realThing.someMethodOfThing();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public void initialize() {
        realThing = longAndCostlyInitializationOfThing();
        countDownLatch.countDown();
    }

    public Thing getRealThing() {
        return realThing;
    }
}

これにより、マップがロックされますが、必要に応じてプロキシを作成して配置するための短い時間だけです。

プロキシのコードが面倒になる場合があります。その場合は、リフレクションを使用してプロキシを作成する方がよい場合があります (「 」を参照java.lang.reflect.Proxy) 。

于 2013-01-04T18:10:27.423 に答える
0

これらのソリューションを試してみましたが、他のシナリオでは機能しないとは言っていませんが、実装のある時点で失敗しました。

私が最終的にやったのは、ConcurrentMapを使用して、リソースが既に要求されているかどうかを確認することでした。そうでない場合は、作成されて別の場所に保存されます。

... 
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;

... 
private ConcurrentMap<String, Boolean> created = new ConcurrentMap<>();
....
if ( created.putIfAbsent( id, Boolean.TRUE ) == null ) {
    somewhereElse.put( id, createThing() );
}
于 2013-01-15T20:55:33.960 に答える