web-dev-qa-db-ja.com

整数値での同期

可能性のある複製:
Javaでロックの数を増やす最良の方法は何ですか

整数のID値に基づいてロックしたいとします。この場合、キャッシュから値をプルし、値が存在しない場合はかなりコストのかかる検索/保存を行う関数があります。

既存のコードは同期されておらず、複数の取得/保存操作をトリガーする可能性があります。

//psuedocode
public Page getPage (Integer id){
   Page p = cache.get(id);
   if (p==null)
   {
      p=getFromDataBase(id);
      cache.store(p);
   }
}

私がしたいことは、IDで取得を同期することです。

   if (p==null)
   {
       synchronized (id)
       {
        ..retrieve, store
       }
   }

残念ながら、これは機能しません。2つの別々の呼び出しは同じInteger id値を持つことができ、異なるIntegerオブジェクトを持つことができるため、ロックを共有せず、同期は行われません。

同じ整数インスタンスがあることを保証する簡単な方法はありますか?たとえば、これは機能しますか?

 syncrhonized (Integer.valueOf(id.intValue())){

Integer.valueOf()のjavadocは、同じインスタンスを取得する可能性が高いことを示唆しているようですが、これは保証のようには見えません。

指定されたint値を表すIntegerインスタンスを返します。新しいIntegerインスタンスが必要ない場合、このメソッドは一般的にコンストラクターInteger(int)よりも優先して使用する必要があります。このメソッドは、頻繁に要求される値をキャッシュすることにより、スペースと時間のパフォーマンスが大幅に向上するためです。

それで、同じであることが保証されているIntegerインスタンスを取得する方法に関する提案はありますか?Intにキー設定されたLockオブジェクトのWeakHashMapを保持するなどのより複雑な解決策はありませんか? (それで何も問題はありません、私が見逃しているよりも明白なワンライナーがあるはずです)。

32
Steve B.

どのインスタンスが同じで、どのインスタンスが異なるかを制御できないため、Integerで同期する必要はありません。 Javaは、さまざまなJVM間で信頼できるこのような機能を提供していません(小さな範囲で整数を使用している場合を除く)。整数で同期する必要がある場合は、 MapまたはSet of Integerを保持して、必要なインスタンスが正確に取得されることを保証できます。

同期するために、HashMapをキーとするIntegerに格納された新しいオブジェクトを作成することをお勧めします。このようなもの:

public Page getPage(Integer id) {
  Page p = cache.get(id);
  if (p == null) {
    synchronized (getCacheSyncObject(id)) {
      p = getFromDataBase(id);
      cache.store(p);
    }
  }
}

private ConcurrentMap<Integer, Integer> locks = new ConcurrentHashMap<Integer, Integer>();

private Object getCacheSyncObject(final Integer id) {
  locks.putIfAbsent(id, id);
  return locks.get(id);
}

このコードを説明するために、ConcurrentMapを使用してputIfAbsentを使用できるようにします。あなたはこれを行うことができます:

  locks.putIfAbsent(id, new Object());

ただし、アクセスごとにオブジェクトを作成するための(わずかな)コストが発生します。それを避けるために、私はInteger自体をMapに保存します。これは何を達成しますか?これがInteger自体を使用するのと何が違うのですか?

Mapからget()を実行すると、キーはequals()と比較されます(または、少なくとも使用される方法はequals()を使用することと同等です)。同じ値の2つの異なるIntegerインスタンスは互いに等しくなります。したがって、 "new Integer(5)"の任意の数の異なるIntegerインスタンスをgetCacheSyncObjectへのパラメーターとして渡すことができ、常に、その値を含む、渡された最初のインスタンスのみが返されます。

Integerで同期したくない理由はいくつかあります...複数のスレッドがIntegerオブジェクトで同期していて、意図的に同じロックを使用している場合、デッドロックになる可能性があります。異なるロックを使用します。このリスクを修正するには、

  locks.putIfAbsent(id, new Object());

バージョン、つまりキャッシュへのアクセスごとに(非常に)小さなコストが発生します。これを行うと、他のクラスが同期しないオブジェクトに対してこのクラスが同期を行うことが保証されます。常に良いことです。

50
Eddie

ConcurrentHashMapなどのスレッドセーフマップを使用します。これにより、マップを安全に操作できますが、実際の計算には別のロックを使用します。このようにして、単一のマップと同時に複数の計算を実行できます。

使用する ConcurrentMap.putIfAbsent、ただし実際の値を配置する代わりに、計算量の少ない構成でFutureを使用します。おそらくFutureTask実装。計算を実行してから結果をgetします。これにより、完了するまでスレッドセーフにブロックされます。

Integer.valueOf()は、限られた範囲のキャッシュされたインスタンスのみを返します。範囲を指定していませんが、通常、これは機能しません。

ただし、値が正しい範囲内にある場合でも、このアプローチをとらないことを強くお勧めします。これらのキャッシュされたIntegerインスタンスはどのコードでも使用できるため、同期を完全に制御できず、デッドロックが発生する可能性があります。これは、人々がString.intern()の結果をロックしようとしているのと同じ問題です。

最良のロックはプライベート変数です。コードのみが参照できるため、デッドロックが発生しないことを保証できます。

ちなみに、WeakHashMapを使用しても機能しません。キーとして機能するインスタンスが参照されていない場合、ガベージコレクションされます。強く参照されている場合は、直接使用できます。

4
erickson

IntegerでSynchronizedを使用すると、設計上、実際には間違っているように聞こえます。

検索/保存時にのみ各アイテムを個別に同期する必要がある場合は、セットを作成して、現在ロックされているアイテムをそこに保存できます。つまり、

// this contains only those IDs that are currently locked, that is, this
// will contain only very few IDs most of the time
Set<Integer> activeIds = ...

Object retrieve(Integer id) {
    // acquire "lock" on item #id
    synchronized(activeIds) {
        while(activeIds.contains(id)) {
            try { 
                activeIds.wait();   
            } catch(InterruptedExcption e){...}
        }
        activeIds.add(id);
    }
    try {

        // do the retrieve here...

        return value;

    } finally {
        // release lock on item #id
        synchronized(activeIds) { 
            activeIds.remove(id); 
            activeIds.notifyAll(); 
        }   
    }   
}

同じことが店に行きます。

つまり、この問題を必要な方法で正確に解決するコードは1行もありません。

3
Antonio

IDからミューテックスを作成するための this code を見ることができます。コードは文字列ID用に作成されましたが、Integerオブジェクト用に簡単に編集できました。

1
McDowell

スティーブ、

提案されたコードには、同期に関する一連の問題があります。 (Antonio'sも同様です)。

要約する:

  1. 高価なオブジェクトをキャッシュする必要があります。
  2. あるスレッドが取得を行っている間、別のスレッドが同じオブジェクトを取得しようとしないことを確認する必要があります。
  3. オブジェクトを取得しようとするnスレッドの場合、1つのオブジェクトのみが取得されて返されます。
  4. これは、異なるオブジェクトを要求するスレッドが互いに競合しないことを意味します。

これを実現するための擬似コード(ConcurrentHashMapをキャッシュとして使用):

ConcurrentMap<Integer, Java.util.concurrent.Future<Page>> cache = new ConcurrentHashMap<Integer, Java.util.concurrent.Future<Page>>;

public Page getPage(Integer id) {
    Future<Page> myFuture = new Future<Page>();
    cache.putIfAbsent(id, myFuture);
    Future<Page> actualFuture = cache.get(id);
    if ( actualFuture == myFuture ) {
        // I am the first w00t!
        Page page = getFromDataBase(id);
        myFuture.set(page);
    }
    return actualFuture.get();
}

注意:

  1. Java.util.concurrent.Futureはインターフェースです
  2. Java.util.concurrent.Futureには実際にはset()はありませんが、Futureを実装する既存のクラスを見て、独自のFutureを実装する方法を理解してください(またはFutureTaskを使用してください)。
  3. 実際の検索をワーカースレッドにプッシュすることは、ほぼ間違いなく良い考えです。
1
Pat

Java Concurrency in Practice :「効率的でスケーラブルな結果キャッシュの構築」のセクション5.6を参照してください。それはあなたが解決しようとしている正確な問題を扱います。特に、memoizerパターンを確認してください。

alt text
(ソース: md.ed

1
Julien Chastang

さまざまな回答からわかるように、この猫の皮をむく方法はいくつかあります。

  • GoetzらのFutureTasksのキャッシュを維持するアプローチは、「とにかく何かをキャッシュする」というこのような状況で非常にうまく機能するので、FutureTaskオブジェクトのマップを作成することを気にしないでください(そして、マップの増加を気にした場合は、少なくともそれを同時に剪定することは簡単です)
  • 「IDをロックする方法」に対する一般的な回答として、Antonioによって概説されているアプローチには、ロックのマップが追加/削除されるときに明らかであるという利点があります。

Antonioのimplementationの潜在的な問題に注意する必要がある場合があります。つまり、notifyAll()はallを待っているスレッドをウェイクアップしますone =それらが利用可能になり、高い競合状況では十分にスケーリングされない可能性があります。原則として、現在ロックされているIDごとにConditionオブジェクトを設定することで、これを修正できると思います。これにより、待機/シグナルを送信します。もちろん、実際には、常に複数のIDが待機されることがほとんどない場合、これは問題ではありません。

1
Neil Coffey

Integerオブジェクトをキーとして持つConcurrentHashMapはどうでしょうか?

1
starblue