特に数百人のユーザーが同じページにアクセスし、このページで約10秒ごとに更新をヒットすることを期待している機能で、いくつかの負荷/パフォーマンステストを行っている途中のWebアプリケーションがあります。この機能で改善できることの1つは、データが変更されていないため、Webサービスからの応答を一定期間キャッシュすることでした。
この基本的なキャッシングを実装した後、さらにいくつかのテストを行ったところ、同時スレッドが同時にキャッシュにアクセスする方法を考慮していないことがわかりました。約100ミリ秒以内に、約50のスレッドがキャッシュからオブジェクトをフェッチしようとし、期限切れであることを検出し、Webサービスにアクセスしてデータをフェッチし、オブジェクトをキャッシュに戻していることがわかりました。
元のコードは次のようになります。
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
final String key = "Data-" + email;
SomeData[] data = (SomeData[]) StaticCache.get(key);
if (data == null) {
data = service.getSomeDataForEmail(email);
StaticCache.set(key, data, CACHE_TIME);
}
else {
logger.debug("getSomeDataForEmail: using cached object");
}
return data;
}
したがって、key
のオブジェクトが期限切れになったときに1つのスレッドのみがWebサービスを呼び出していることを確認するには、キャッシュのget/set操作を同期する必要があると思いました。キャッシュキーを使用すると、同期するオブジェクトの適切な候補(この方法では、電子メール[email protected]に対するこのメソッドの呼び出しは、a @ a.comへのメソッド呼び出しによってブロックされません)。
メソッドを次のように更新しました。
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
SomeData[] data = null;
final String key = "Data-" + email;
synchronized(key) {
data =(SomeData[]) StaticCache.get(key);
if (data == null) {
data = service.getSomeDataForEmail(email);
StaticCache.set(key, data, CACHE_TIME);
}
else {
logger.debug("getSomeDataForEmail: using cached object");
}
}
return data;
}
「同期ブロックの前」、「同期ブロック内」、「同期ブロックを出ようとしている」、「同期ブロックの後」などのログ行も追加したので、get/set操作を効果的に同期しているかどうかを確認できます。
しかし、これはうまくいったようには見えません。私のテストログには次のような出力があります:
(ログ出力は「スレッド名」「ロガー名」「メッセージ」です)
http-80-Processor253 jsp.view-page-getSomeDataForEmail:同期ブロックに入ります
http-80-Processor253 jsp.view-page-getSomeDataForEmail:同期ブロック内
http-80-Processor253 cache.StaticCache-get:キー[[email protected]]のオブジェクトが期限切れです
http-80-Processor253 cache.StaticCache-get:key [[email protected]]戻り値[null]
http-80-Processor263 jsp.view-page-getSomeDataForEmail:同期ブロックに入ります
http-80-Processor263 jsp.view-page-getSomeDataForEmail:同期ブロック内
http-80-Processor263 cache.StaticCache-get:キー[[email protected]]のオブジェクトが期限切れです
http-80-Processor263 cache.StaticCache-get:key [[email protected]]戻り値[null]
http-80-Processor131 jsp.view-page-getSomeDataForEmail:同期ブロックに入ります
http-80-Processor131 jsp.view-page-getSomeDataForEmail:同期ブロック内
http-80-Processor131 cache.StaticCache-get:キー[[email protected]]のオブジェクトが期限切れです
http-80-Processor131 cache.StaticCache-get:key [[email protected]]戻り値[null]
http-80-Processor104 jsp.view-page-getSomeDataForEmail:同期ブロック内
http-80-Processor104 cache.StaticCache-get:キー[[email protected]]のオブジェクトが期限切れです
http-80-Processor104 cache.StaticCache-get:key [[email protected]]戻り値[null]
http-80-Processor252 jsp.view-page-getSomeDataForEmail:同期ブロックに入ります
http-80-Processor283 jsp.view-page-getSomeDataForEmail:同期ブロックに入ります
http-80-Processor2 jsp.view-page-getSomeDataForEmail:同期ブロックに入ります
http-80-Processor2 jsp.view-page-getSomeDataForEmail:同期ブロック内
Get/set操作の周りの同期ブロックに一度に1つだけスレッドが入る/出るのを見たいと思っていました。
Stringオブジェクトの同期に問題はありますか?キャッシュキーは操作に固有であるため、適切な選択だと思いました。final String key
がメソッド内で宣言されていても、各スレッドが同じオブジェクトなので、この単一のオブジェクトで同期します。
ここで何が悪いのですか?
pdate:ログをさらに見ると、キーが常に同じである同じ同期ロジックを持つメソッドのように見えます。
final String key = "blah";
...
synchronized(key) { ...
同じ同時実行性の問題は発生しません。一度に1つのスレッドのみがブロックに入ります。
アップデート2:助けてくれてありがとう!文字列のintern()
ingに関する最初の回答を受け入れました。これにより、key
の値が同じだったため、同期できないブロックに複数のスレッドが入ってはいけないと思ったときに、最初の問題が解決しました。
他の人が指摘したように、intern()
をそのような目的で使用し、それらの文字列を同期することは、実際に悪い考えであることがわかります-Webアプリケーションに対してJMeterテストを実行して予想される負荷をシミュレートすると、使用されたヒープが表示されましたサイズは20分弱でほぼ1 GBに増加します。
現在、私はメソッド全体を同期するだけの簡単なソリューションを使用しています-しかし、私はmartinprobstとMBCookによって提供されるコードサンプルのようにreallyですが、約7つの同様のgetData()
メソッドがあるのでこのクラスは現在(Webサービスからの約7つの異なるデータが必要なため)、各メソッドのロックの取得と解放に関するほぼ重複したロジックを追加したくありませんでした。しかし、これは間違いなく、非常に、将来の使用のための非常に貴重な情報です。これらは、最終的にこのようなスレッドセーフのような操作を行うための最善の方法の正しい答えだと思います。できれば、これらの答えに投票してください!
私の頭脳を完全にギアに入れずに、あなたが言うことのクイックスキャンから、それはあなたがあなたのストリングをインターンする必要があるように見えます:
final String firstkey = "Data-" + email;
final String key = firstkey.intern();
それ以外の場合、同じ値を持つ2つの文字列は必ずしも同じオブジェクトであるとは限りません。
VMの奥深くで、intern()がロックを取得する必要がある場合があるため、これは新しい競合ポイントを導入する可能性があることに注意してください。この領域で最新のVMがどのように見えるかはわかりませんが、ひどく最適化されていることを願っています。
StaticCacheはまだスレッドセーフである必要があることを知っていると思います。ただし、getSomeDataForEmailを呼び出すときにキーだけでなくキャッシュをロックした場合に比べて、競合はごくわずかです。
質問の更新への応答:
文字列リテラルは常に同じオブジェクトを生成するためです。 Dave Costaがコメントで指摘しているのは、それよりも優れているということです。リテラルは常に正規表現を生成します。したがって、プログラム内のどこかに同じ値を持つすべての文字列リテラルは、同じオブジェクトを生成します。
編集
インターン文字列での同期は実際には非常に悪い考えです-一部には、インターン文字列を作成することでそれらを永続的に存在させることが許可されているため、および一部では複数ビットのコードがプログラムがインターン文字列で同期し、それらのコードのビット間に依存関係があり、デッドロックやその他のバグを防ぐことができない場合があります。
キー文字列ごとにロックオブジェクトを格納することでこれを回避する戦略は、私が入力する他の回答で開発されています。
ここに代替案があります-それはまだ単一のロックを使用しますが、とにかくキャッシュ用にそれらの1つが必要になることを知っています、そしてあなたは5000ではなく50スレッドについて話していたので、致命的ではないかもしれません。また、ここでのパフォーマンスのボトルネックは、DoSlowThing()でのI/Oのブロックが遅いため、シリアル化されていないことのメリットが大きいと想定しています。それがボトルネックでない場合は、次のようにします。
明らかに、このアプローチは、使用前にスケーラビリティについてソークテストを行う必要があります-私は何も保証しません。
このコードでは、StaticCacheが同期されているか、スレッドセーフである必要はありません。他のコード(たとえば、古いデータのスケジュールされたクリーンアップ)がキャッシュにアクセスする場合は、再検討する必要があります。
IN_PROGRESSはダミー値です-正確ではありませんが、コードは単純で、2つのハッシュテーブルを節約できます。その場合、アプリが何をしたいのかわからないため、InterruptedExceptionは処理されません。また、与えられたキーに対してDoSlowThing()が一貫して失敗する場合、このコードはそのままでは正確ではありません。失敗の基準とは何か、また一時的または永続的なものになる可能性があるかどうかわからないため、これも処理せず、スレッドが永久にブロックされないようにします。実際には、おそらく理由により「使用不可」を示すデータ値と、再試行するときのタイムアウトをキャッシュに入れたい場合があります。
// do not attempt double-check locking here. I mean it.
synchronized(StaticObject) {
data = StaticCache.get(key);
while (data == IN_PROGRESS) {
// another thread is getting the data
StaticObject.wait();
data = StaticCache.get(key);
}
if (data == null) {
// we must get the data
StaticCache.put(key, IN_PROGRESS, TIME_MAX_VALUE);
}
}
if (data == null) {
// we must get the data
try {
data = server.DoSlowThing(key);
} finally {
synchronized(StaticObject) {
// WARNING: failure here is fatal, and must be allowed to terminate
// the app or else waiters will be left forever. Choose a suitable
// collection type in which replacing the value for a key is guaranteed.
StaticCache.put(key, data, CURRENT_TIME);
StaticObject.notifyAll();
}
}
}
キャッシュに何かが追加されるたびに、すべてのスレッドがウェイクアップしてキャッシュをチェックするので(どのキーの後でも)、問題の少ないアルゴリズムでパフォーマンスを向上させることができます。ただし、その作業の多くは、I/Oでの大量のアイドルCPU時間のブロック中に行われるため、問題にはならない可能性があります。
このコードは、複数のキャッシュで使用するために共通にすることができます。キャッシュとそれに関連するロック、それが返すデータ、IN_PROGRESSダミー、および実行するのに遅い操作に適切な抽象化を定義する場合。すべてをキャッシュ上のメソッドにまとめることは悪い考えではないかもしれません。
インターンされた文字列で同期するのは良い考えではないかもしれません-文字列をインターンすると、文字列はグローバルオブジェクトに変わり、アプリケーションの異なる部分で同じインターンされた文字列で同期すると、本当に奇妙になり、基本的に、デッドロックなどのデバッグできない同期の問題。それはありそうにないように見えるかもしれませんが、それが起こるとき、あなたは本当にねじ込まれています。一般的なルールとして、モジュールの外部のコードでロックされないことが絶対的に確実であるローカルオブジェクトでのみ同期します。
あなたのケースでは、同期ハッシュテーブルを使用して、キーのロックオブジェクトを格納できます。
例えば。:
Object data = StaticCache.get(key, ...);
if (data == null) {
Object lock = lockTable.get(key);
if (lock == null) {
// we're the only one looking for this
lock = new Object();
synchronized(lock) {
lockTable.put(key, lock);
// get stuff
lockTable.remove(key);
}
} else {
synchronized(lock) {
// just to wait for the updater
}
data = StaticCache.get(key);
}
} else {
// use from cache
}
このコードには競合状態があり、2つのスレッドがオブジェクトを順番にロックテーブルに入れます。ただし、これで問題になることはありません。これは、Webサービスを呼び出してキャッシュを更新するスレッドが1つしかないため、問題にはなりません。
しばらくしてからキャッシュを無効にする場合は、ロックからデータを取得した後で、データがnullかどうかをもう一度確認する必要があります(ロック!= nullの場合)。
または、はるかに簡単に、キャッシュルックアップメソッド全体( "getSomeDataByEmail")を同期させることができます。これは、すべてのスレッドがキャッシュにアクセスするときに同期する必要があることを意味します。これは、パフォーマンスの問題である可能性があります。しかし、いつものように、最初にこの簡単な解決策を試して、それが本当に問題かどうかを確認してください!多くの場合、同期するよりも結果の処理に多くの時間を費やすため、そうするべきではありません。
文字列は、同期に適した候補ではありませんではありません。文字列IDで同期する必要がある場合は、文字列を使用してミューテックスを作成します(「 IDでの同期 」を参照)。そのアルゴリズムのコストが価値があるかどうかは、サービスの呼び出しに重要なI/Oが含まれているかどうかによって異なります。
また:
他の人たちは文字列を解釈することを提案しており、それはうまくいきます。
問題は、Javaはインターンされた文字列を保持する必要があるということです。参照を保持していなくても、次回誰かがそれを使用するときに値が同じである必要があるため、これを行うと言われましたこれは、すべての文字列のインターンがメモリを消費し始める可能性があることを意味します。これは、記述している負荷で大きな問題になる可能性があります。
これに対する2つの解決策を見てきました。
別のオブジェクトで同期できます
電子メールの代わりに、電子メールの値を変数として保持する電子メール(たとえば、ユーザーオブジェクト)を保持するオブジェクトを作成します。人物を表す別のオブジェクトが既にある場合(たとえば、ユーザーのメールに基づいてDBから何かをすでにプルしたとしましょう)、それを使用できます。 equalsメソッドとhashcodeメソッドを実装することで、静的なcache.contains()を実行するときにJavaがオブジェクトを同じと見なして、データがすでにキャッシュにあるかどうかを確認することができます(キャッシュで同期する必要があります)。
実際には、オブジェクトをロックするための2番目のマップを保持できます。このようなもの:
Map<String, Object> emailLocks = new HashMap<String, Object>();
Object lock = null;
synchronized (emailLocks) {
lock = emailLocks.get(emailAddress);
if (lock == null) {
lock = new Object();
emailLocks.put(emailAddress, lock);
}
}
synchronized (lock) {
// See if this email is in the cache
// If so, serve that
// If not, generate the data
// Since each of this person's threads synchronizes on this, they won't run
// over eachother. Since this lock is only for this person, it won't effect
// other people. The other synchronized block (on emailLocks) is small enough
// it shouldn't cause a performance problem.
}
これにより、同じ電子メールアドレスで15のフェッチが同時に実行されなくなります。あまりにも多くのエントリがemailLocksマップに登録されるのを防ぐために何かが必要になります。 Apache Commonsの LRUMap sを使用すると、それが可能になります。
これには多少の調整が必要ですが、問題が解決する可能性があります。
別のキーを使用してください
エラーが発生しても構わないと思っている場合(これがどれほど重要かはわかりません)、文字列のハッシュコードをキーとして使用できます。 intはインターンする必要はありません。
まとめ
これがお役に立てば幸いです。スレッド化は楽しいですね。また、セッションを使用して、「これをすでに見つけている」という意味の値を設定し、2番目(3番目、N番目)のスレッドがを作成するか、結果が表示されるのを待つ必要があるかどうかを確認することもできます。キャッシュ内。 3つの提案があったと思います。
1.5の同時実行ユーティリティを使用して、複数の同時アクセスを許可するように設計されたキャッシュと単一の追加ポイント(つまり、高価なオブジェクトの「作成」を実行するスレッドが1つだけ)を提供できます。
private ConcurrentMap<String, Future<SomeData[]> cache;
private SomeData[] getSomeDataByEmail(final WebServiceInterface service, final String email) throws Exception {
final String key = "Data-" + email;
Callable<SomeData[]> call = new Callable<SomeData[]>() {
public SomeData[] call() {
return service.getSomeDataForEmail(email);
}
}
FutureTask<SomeData[]> ft; ;
Future<SomeData[]> f = cache.putIfAbsent(key, ft= new FutureTask<SomeData[]>(call)); //atomic
if (f == null) { //this means that the cache had no mapping for the key
f = ft;
ft.run();
}
return f.get(); //wait on the result being available if it is being calculated in another thread
}
明らかに、これはあなたが望むように例外を処理せず、キャッシュにはエビクションが組み込まれていません。おそらく、StaticCacheクラスを変更するためのベースとして使用できます。
これは安全な短いJava 8ソリューションであり、同期に専用ロックオブジェクトのマップを使用します。
private static final Map<String, Object> keyLocks = new ConcurrentHashMap<>();
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
final String key = "Data-" + email;
synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
SomeData[] data = StaticCache.get(key);
if (data == null) {
data = service.getSomeDataForEmail(email);
StaticCache.set(key, data);
}
}
return data;
}
キーとロックオブジェクトがマップに永久に保持されるという欠点があります。
これは次のように回避できます。
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
final String key = "Data-" + email;
synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
try {
SomeData[] data = StaticCache.get(key);
if (data == null) {
data = service.getSomeDataForEmail(email);
StaticCache.set(key, data);
}
} finally {
keyLocks.remove(key); // vulnerable to race-conditions
}
}
return data;
}
しかし、その後、人気のあるキーは、マップに常に再挿入され、ロックオブジェクトが再割り当てされます。
Update:これにより、2つのスレッドが同じキーの異なるセクションの同期セクションに同時に入ると、競合状態が発生する可能性があります。
したがって、使用する方が安全で効率的かもしれません 有効期限が切れるGuavaキャッシュ :
private static final LoadingCache<String, Object> keyLocks = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES) // max lock time ever expected
.build(CacheLoader.from(Object::new));
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
final String key = "Data-" + email;
synchronized (keyLocks.getUnchecked(key)) {
SomeData[] data = StaticCache.get(key);
if (data == null) {
data = service.getSomeDataForEmail(email);
StaticCache.set(key, data);
}
}
return data;
}
ここでは、StaticCache
はスレッドセーフであり、異なるキーの同時読み取りおよび書き込みの影響を受けないと想定されていることに注意してください。
ehcache などの適切なキャッシュフレームワークを使用します。
適切なキャッシュの実装は、一部の人々が信じているほど簡単ではありません。
String.intern()がメモリリークの原因であるというコメントに関しては、実際にはそうではありません。インターンされた文字列areガベージコレクション、特定のJVM(Sun)ではフルGCによってのみアクセスされるPermスペースに格納されるため、ガベージコレクションに時間がかかる場合があります。
この質問は私には少し広すぎるように思われるため、同じように広い範囲の回答を促しました。だから私は答えようとします 質問 リダイレクトされましたが、残念ながら重複して閉じられました。
_public class ValueLock<T> {
private Lock lock = new ReentrantLock();
private Map<T, Condition> conditions = new HashMap<T, Condition>();
public void lock(T t){
lock.lock();
try {
while (conditions.containsKey(t)){
conditions.get(t).awaitUninterruptibly();
}
conditions.put(t, lock.newCondition());
} finally {
lock.unlock();
}
}
public void unlock(T t){
lock.lock();
try {
Condition condition = conditions.get(t);
if (condition == null)
throw new IllegalStateException();// possibly an attempt to release what wasn't acquired
conditions.remove(t);
condition.signalAll();
} finally {
lock.unlock();
}
}
_
(外部)lock
操作時に(内部)ロックが取得され、短時間マップに排他的にアクセスします。対応するオブジェクトが既にマップ内にある場合、現在のスレッドは待機します。それ以外の場合は、新しいCondition
をマップに配置し、(内部)ロックを解放して続行すると、(外部)ロックが取得されたと見なされます。 (外部)unlock
操作は、最初に(内部)ロックを取得し、Condition
を通知してから、オブジェクトをマップから削除します。
クラスはMap
の同時バージョンを使用しません。これは、クラスへのすべてのアクセスが単一の(内部)ロックによって保護されるためです。
このクラスのlock()
メソッドのセマンティクスはReentrantLock.lock()
のセマンティクスとは異なることに注意してください。対になったlock()
の呼び出しを繰り返すと、[unlock()
の呼び出しがハングします。現在のスレッドは無期限です。
OPで説明されている状況に該当する可能性のある使用例
_ ValueLock<String> lock = new ValueLock<String>();
// ... share the lock
String email = "...";
try {
lock.lock(email);
//...
} finally {
lock.unlock(email);
}
_
呼び出し:
final String key = "Data-" + email;
メソッドが呼び出されるたびに新しいオブジェクトを作成します。そのオブジェクトはロックに使用するオブジェクトであり、このメソッドを呼び出すたびに新しいオブジェクトが作成されるため、キーに基づいてマップへのアクセスを実際に同期しているわけではありません。
これはあなたの編集をさらに説明します。静的な文字列がある場合、それは機能します。
Intern()を使用すると問題が解決します。これは、Stringクラスによって保持されている内部プールから文字列を返すため、2つの文字列が等しい場合、プール内の文字列が使用されることを保証します。見る
http://Java.Sun.com/j2se/1.4.2/docs/api/Java/lang/String.html#intern()
主な問題は、同じ値を持つStringのインスタンスが複数存在する可能性があることだけではありません。主な問題は、StaticCacheオブジェクトにアクセスするために同期するモニターが1つだけ必要なことです。そうしないと、複数のスレッドがStaticCacheを同時に変更する可能性があります(異なるキーの下ではあります)。これは、おそらく同時変更をサポートしていません。
これはかなり遅いですが、ここに示されている多くの誤ったコードがあります。
この例では:
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
SomeData[] data = null;
final String key = "Data-" + email;
synchronized(key) {
data =(SomeData[]) StaticCache.get(key);
if (data == null) {
data = service.getSomeDataForEmail(email);
StaticCache.set(key, data, CACHE_TIME);
}
else {
logger.debug("getSomeDataForEmail: using cached object");
}
}
return data;
}
同期のスコープが誤っています。 get/put APIをサポートする静的キャッシュの場合、キャッシュへの安全なアクセスのために、少なくともgetおよびgetIfAbsentPutタイプの操作に関する同期が必要です。同期の範囲はキャッシュ自体になります。
データ要素自体を更新する必要がある場合、同期の追加レイヤーが追加されます。これは、個々のデータ要素上で行う必要があります。
SynchronizedMapは明示的な同期の代わりに使用できますが、注意が必要です。間違ったAPIが使用された場合(putIfAbsentの代わりにgetおよびput)、同期されたマップを使用しているにもかかわらず、操作に必要な同期がありません。 putIfAbsentの使用により発生する複雑さに注意してください。どちらか一方が必要でない場合でも(キャッシュの内容が調べられるまでput値が必要かどうかをputが認識できないため)、または注意が必要です。委任の使用(たとえば、Futureを使用します。これは機能しますが、多少不一致です。以下を参照してください)。必要に応じて、プット値がオンデマンドで取得されます。
Futuresを使用することは可能ですが、かなり扱いにくく、おそらく少しやりすぎです。フューチャーAPIは、非同期操作、特に、すぐには完了しない可能性のある操作のコアです。 Involving Futureはおそらくスレッド作成のレイヤーを追加します-おそらく不必要な複雑さを追加します。
このタイプの操作にFutureを使用することの主な問題は、Futureが本質的にマルチスレッドに関係していることです。新しいスレッドが不要な場合にFutureを使用すると、Futureの多くの機構が無視され、APIが非常に重いAPIになります。
2019年最新アップデート、
Javaで同期を実装する新しい方法を検索している場合、この答えが適しています。
Anatoliy Korovinによるこの驚くべきブログを見つけました。これは、シンクロナイズドを深く理解するのに役立ちます。
Javaでオブジェクトの値によってブロックを同期する方法 。
これは、新しい開発者もこれが役立つことを願っています
ユーザーに提供され、x分ごとに再生成される静的HTMLページをレンダリングしないのはなぜですか?
文字列オブジェクトで同期する別の方法:
String cacheKey = ...;
Object obj = cache.get(cacheKey)
if(obj==null){
synchronized (Integer.valueOf(Math.abs(cacheKey.hashCode()) % 127)){
obj = cache.get(cacheKey)
if(obj==null){
//some cal obtain obj value,and put into cache
}
}
}
あなたの場合、次のようなものを使うことができます(これはメモリをリークしません):
private Synchronizer<String> synchronizer = new Synchronizer();
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
String key = "Data-" + email;
return synchronizer.synchronizeOn(key, () -> {
SomeData[] data = (SomeData[]) StaticCache.get(key);
if (data == null) {
data = service.getSomeDataForEmail(email);
StaticCache.set(key, data, CACHE_TIME);
} else {
logger.debug("getSomeDataForEmail: using cached object");
}
return data;
});
}
それを使用するには、依存関係を追加するだけです:
compile 'com.github.matejtymes:javafixes:1.3.0'
また、必要がない場合は、文字列連結を完全に取り除くことをお勧めします。
final String key = "Data-" + email;
キーの先頭に追加の「Data-」が必要な電子メールアドレスを使用する、キャッシュ内の他のオブジェクト/タイプのオブジェクトはありますか?
そうでなければ、私はそれを作ります
final String key = email;
また、余分な文字列の作成もすべて回避できます。
文字列を含む任意のキーをロック/同期できる小さなロッククラスを追加しました。
Java 8、Java 6、および小さなテストの実装を参照してください。
Java 8:
public class DynamicKeyLock<T> implements Lock
{
private final static ConcurrentHashMap<Object, LockAndCounter> locksMap = new ConcurrentHashMap<>();
private final T key;
public DynamicKeyLock(T lockKey)
{
this.key = lockKey;
}
private static class LockAndCounter
{
private final Lock lock = new ReentrantLock();
private final AtomicInteger counter = new AtomicInteger(0);
}
private LockAndCounter getLock()
{
return locksMap.compute(key, (key, lockAndCounterInner) ->
{
if (lockAndCounterInner == null) {
lockAndCounterInner = new LockAndCounter();
}
lockAndCounterInner.counter.incrementAndGet();
return lockAndCounterInner;
});
}
private void cleanupLock(LockAndCounter lockAndCounterOuter)
{
if (lockAndCounterOuter.counter.decrementAndGet() == 0)
{
locksMap.compute(key, (key, lockAndCounterInner) ->
{
if (lockAndCounterInner == null || lockAndCounterInner.counter.get() == 0) {
return null;
}
return lockAndCounterInner;
});
}
}
@Override
public void lock()
{
LockAndCounter lockAndCounter = getLock();
lockAndCounter.lock.lock();
}
@Override
public void unlock()
{
LockAndCounter lockAndCounter = locksMap.get(key);
lockAndCounter.lock.unlock();
cleanupLock(lockAndCounter);
}
@Override
public void lockInterruptibly() throws InterruptedException
{
LockAndCounter lockAndCounter = getLock();
try
{
lockAndCounter.lock.lockInterruptibly();
}
catch (InterruptedException e)
{
cleanupLock(lockAndCounter);
throw e;
}
}
@Override
public boolean tryLock()
{
LockAndCounter lockAndCounter = getLock();
boolean acquired = lockAndCounter.lock.tryLock();
if (!acquired)
{
cleanupLock(lockAndCounter);
}
return acquired;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
LockAndCounter lockAndCounter = getLock();
boolean acquired;
try
{
acquired = lockAndCounter.lock.tryLock(time, unit);
}
catch (InterruptedException e)
{
cleanupLock(lockAndCounter);
throw e;
}
if (!acquired)
{
cleanupLock(lockAndCounter);
}
return acquired;
}
@Override
public Condition newCondition()
{
LockAndCounter lockAndCounter = locksMap.get(key);
return lockAndCounter.lock.newCondition();
}
}
Java 6:
パブリッククラスDynamicKeyLockはLock {private final static ConcurrentHashMap locksMap = new ConcurrentHashMap();を実装します。秘密の最終Tキー。
public DynamicKeyLock(T lockKey) {
this.key = lockKey;
}
private static class LockAndCounter {
private final Lock lock = new ReentrantLock();
private final AtomicInteger counter = new AtomicInteger(0);
}
private LockAndCounter getLock()
{
while (true) // Try to init lock
{
LockAndCounter lockAndCounter = locksMap.get(key);
if (lockAndCounter == null)
{
LockAndCounter newLock = new LockAndCounter();
lockAndCounter = locksMap.putIfAbsent(key, newLock);
if (lockAndCounter == null)
{
lockAndCounter = newLock;
}
}
lockAndCounter.counter.incrementAndGet();
synchronized (lockAndCounter)
{
LockAndCounter lastLockAndCounter = locksMap.get(key);
if (lockAndCounter == lastLockAndCounter)
{
return lockAndCounter;
}
// else some other thread beat us to it, thus try again.
}
}
}
private void cleanupLock(LockAndCounter lockAndCounter)
{
if (lockAndCounter.counter.decrementAndGet() == 0)
{
synchronized (lockAndCounter)
{
if (lockAndCounter.counter.get() == 0)
{
locksMap.remove(key);
}
}
}
}
@Override
public void lock()
{
LockAndCounter lockAndCounter = getLock();
lockAndCounter.lock.lock();
}
@Override
public void unlock()
{
LockAndCounter lockAndCounter = locksMap.get(key);
lockAndCounter.lock.unlock();
cleanupLock(lockAndCounter);
}
@Override
public void lockInterruptibly() throws InterruptedException
{
LockAndCounter lockAndCounter = getLock();
try
{
lockAndCounter.lock.lockInterruptibly();
}
catch (InterruptedException e)
{
cleanupLock(lockAndCounter);
throw e;
}
}
@Override
public boolean tryLock()
{
LockAndCounter lockAndCounter = getLock();
boolean acquired = lockAndCounter.lock.tryLock();
if (!acquired)
{
cleanupLock(lockAndCounter);
}
return acquired;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
LockAndCounter lockAndCounter = getLock();
boolean acquired;
try
{
acquired = lockAndCounter.lock.tryLock(time, unit);
}
catch (InterruptedException e)
{
cleanupLock(lockAndCounter);
throw e;
}
if (!acquired)
{
cleanupLock(lockAndCounter);
}
return acquired;
}
@Override
public Condition newCondition()
{
LockAndCounter lockAndCounter = locksMap.get(key);
return lockAndCounter.lock.newCondition();
}
}
テスト:
public class DynamicKeyLockTest
{
@Test
public void testDifferentKeysDontLock() throws InterruptedException
{
DynamicKeyLock<Object> lock = new DynamicKeyLock<>(new Object());
lock.lock();
AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
try
{
new Thread(() ->
{
DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(new Object());
anotherLock.lock();
try
{
anotherThreadWasExecuted.set(true);
}
finally
{
anotherLock.unlock();
}
}).start();
Thread.sleep(100);
}
finally
{
Assert.assertTrue(anotherThreadWasExecuted.get());
lock.unlock();
}
}
@Test
public void testSameKeysLock() throws InterruptedException
{
Object key = new Object();
DynamicKeyLock<Object> lock = new DynamicKeyLock<>(key);
lock.lock();
AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
try
{
new Thread(() ->
{
DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(key);
anotherLock.lock();
try
{
anotherThreadWasExecuted.set(true);
}
finally
{
anotherLock.unlock();
}
}).start();
Thread.sleep(100);
}
finally
{
Assert.assertFalse(anotherThreadWasExecuted.get());
lock.unlock();
}
}
}
他の人が同様の問題を抱えている場合、私が知る限り、次のコードが機能します:
import Java.util.Map;
import Java.util.concurrent.ConcurrentHashMap;
import Java.util.concurrent.atomic.AtomicInteger;
import Java.util.function.Supplier;
public class KeySynchronizer<T> {
private Map<T, CounterLock> locks = new ConcurrentHashMap<>();
public <U> U synchronize(T key, Supplier<U> supplier) {
CounterLock lock = locks.compute(key, (k, v) ->
v == null ? new CounterLock() : v.increment());
synchronized (lock) {
try {
return supplier.get();
} finally {
if (lock.decrement() == 0) {
// Only removes if key still points to the same value,
// to avoid issue described below.
locks.remove(key, lock);
}
}
}
}
private static final class CounterLock {
private AtomicInteger remaining = new AtomicInteger(1);
private CounterLock increment() {
// Returning a new CounterLock object if remaining = 0 to ensure that
// the lock is not removed in step 5 of the following execution sequence:
// 1) Thread 1 obtains a new CounterLock object from locks.compute (after evaluating "v == null" to true)
// 2) Thread 2 evaluates "v == null" to false in locks.compute
// 3) Thread 1 calls lock.decrement() which sets remaining = 0
// 4) Thread 2 calls v.increment() in locks.compute
// 5) Thread 1 calls locks.remove(key, lock)
return remaining.getAndIncrement() == 0 ? new CounterLock() : this;
}
private int decrement() {
return remaining.decrementAndGet();
}
}
}
OPの場合、次のように使用されます。
private KeySynchronizer<String> keySynchronizer = new KeySynchronizer<>();
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
String key = "Data-" + email;
return keySynchronizer.synchronize(key, () -> {
SomeData[] existing = (SomeData[]) StaticCache.get(key);
if (existing == null) {
SomeData[] data = service.getSomeDataForEmail(email);
StaticCache.set(key, data, CACHE_TIME);
return data;
}
logger.debug("getSomeDataForEmail: using cached object");
return existing;
});
}
同期されたコードから何も返されない場合、同期メソッドは次のように記述できます。
public void synchronize(T key, Runnable runnable) {
CounterLock lock = locks.compute(key, (k, v) ->
v == null ? new CounterLock() : v.increment());
synchronized (lock) {
try {
runnable.run();
} finally {
if (lock.decrement() == 0) {
// Only removes if key still points to the same value,
// to avoid issue described below.
locks.remove(key, lock);
}
}
}
}