可能性のある複製:
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を保持するなどのより複雑な解決策はありませんか? (それで何も問題はありません、私が見逃しているよりも明白なワンライナーがあるはずです)。
どのインスタンスが同じで、どのインスタンスが異なるかを制御できないため、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());
バージョン、つまりキャッシュへのアクセスごとに(非常に)小さなコストが発生します。これを行うと、他のクラスが同期しないオブジェクトに対してこのクラスが同期を行うことが保証されます。常に良いことです。
ConcurrentHashMap
などのスレッドセーフマップを使用します。これにより、マップを安全に操作できますが、実際の計算には別のロックを使用します。このようにして、単一のマップと同時に複数の計算を実行できます。
使用する ConcurrentMap.putIfAbsent
、ただし実際の値を配置する代わりに、計算量の少ない構成でFuture
を使用します。おそらくFutureTask
実装。計算を実行してから結果をget
します。これにより、完了するまでスレッドセーフにブロックされます。
Integer.valueOf()
は、限られた範囲のキャッシュされたインスタンスのみを返します。範囲を指定していませんが、通常、これは機能しません。
ただし、値が正しい範囲内にある場合でも、このアプローチをとらないことを強くお勧めします。これらのキャッシュされたInteger
インスタンスはどのコードでも使用できるため、同期を完全に制御できず、デッドロックが発生する可能性があります。これは、人々がString.intern()
の結果をロックしようとしているのと同じ問題です。
最良のロックはプライベート変数です。コードのみが参照できるため、デッドロックが発生しないことを保証できます。
ちなみに、WeakHashMap
を使用しても機能しません。キーとして機能するインスタンスが参照されていない場合、ガベージコレクションされます。強く参照されている場合は、直接使用できます。
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行もありません。
IDからミューテックスを作成するための this code を見ることができます。コードは文字列ID用に作成されましたが、Integerオブジェクト用に簡単に編集できました。
スティーブ、
提案されたコードには、同期に関する一連の問題があります。 (Antonio'sも同様です)。
要約する:
これを実現するための擬似コード(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();
}
注意:
Java Concurrency in Practice :「効率的でスケーラブルな結果キャッシュの構築」のセクション5.6を参照してください。それはあなたが解決しようとしている正確な問題を扱います。特に、memoizerパターンを確認してください。
(ソース: md.ed )
さまざまな回答からわかるように、この猫の皮をむく方法はいくつかあります。
Antonioのimplementationの潜在的な問題に注意する必要がある場合があります。つまり、notifyAll()はallを待っているスレッドをウェイクアップしますone =それらが利用可能になり、高い競合状況では十分にスケーリングされない可能性があります。原則として、現在ロックされているIDごとにConditionオブジェクトを設定することで、これを修正できると思います。これにより、待機/シグナルを送信します。もちろん、実際には、常に複数のIDが待機されることがほとんどない場合、これは問題ではありません。
Integerオブジェクトをキーとして持つConcurrentHashMapはどうでしょうか?