特に、数百人のユーザーが同じページにアクセスし、このページで約 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;
}
したがって、オブジェクトの有効期限が切れたときに 1 つのスレッドだけが Web サービスを呼び出していることを確認するにはkey
、キャッシュの get/set 操作を同期する必要があると考えました。同期します (この方法では、電子メール b@b.com に対するこのメソッドへの呼び出しは、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 操作を効果的に同期しているかどうかを判断できました。
ただし、これが機能しているようには見えません。テスト ログには次のような出力があります。
(log output is 'threadname' 'logger name' 'message')
http-80-Processor253 jsp.view-page - getSomeDataForEmail: about to enter synchronization block
http-80-Processor253 jsp.view-page - getSomeDataForEmail: inside synchronization block
http-80-Processor253 cache.StaticCache - get: object at key [SomeData-test@test.com] has expired
http-80-Processor253 cache.StaticCache - get: key [SomeData-test@test.com] returning value [null]
http-80-Processor263 jsp.view-page - getSomeDataForEmail: about to enter synchronization block
http-80-Processor263 jsp.view-page - getSomeDataForEmail: inside synchronization block
http-80-Processor263 cache.StaticCache - get: object at key [SomeData-test@test.com] has expired
http-80-Processor263 cache.StaticCache - get: key [SomeData-test@test.com] returning value [null]
http-80-Processor131 jsp.view-page - getSomeDataForEmail: about to enter synchronization block
http-80-Processor131 jsp.view-page - getSomeDataForEmail: inside synchronization block
http-80-Processor131 cache.StaticCache - get: object at key [SomeData-test@test.com] has expired
http-80-Processor131 cache.StaticCache - get: key [SomeData-test@test.com] returning value [null]
http-80-Processor104 jsp.view-page - getSomeDataForEmail: inside synchronization block
http-80-Processor104 cache.StaticCache - get: object at key [SomeData-test@test.com] has expired
http-80-Processor104 cache.StaticCache - get: key [SomeData-test@test.com] returning value [null]
http-80-Processor252 jsp.view-page - getSomeDataForEmail: about to enter synchronization block
http-80-Processor283 jsp.view-page - getSomeDataForEmail: about to enter synchronization block
http-80-Processor2 jsp.view-page - getSomeDataForEmail: about to enter synchronization block
http-80-Processor2 jsp.view-page - getSomeDataForEmail: inside synchronization block
get/set 操作の前後で同期ブロックに出入りするスレッドを一度に 1 つだけ見たかったのです。
String オブジェクトの同期に問題はありますか? キャッシュキーは操作に固有であるため、良い選択だとfinal String key
思いました。メソッド内で宣言されていても、各スレッドが同じオブジェクトへの参照を取得するため、これで同期されると考えていました単一のオブジェクト。
ここで何が間違っていますか?
更新: ログをさらに調べたところ、次のように、キーが常に同じである同じ同期ロジックを持つメソッドのようです。
final String key = "blah";
...
synchronized(key) { ...
同じ並行性の問題は発生しません。一度に 1 つのスレッドだけがブロックに入ります。
更新 2 : 助けてくれてありがとう! ing Stringsに関する最初の回答を受け入れましたintern()
。これにより、最初の問題が解決されました。複数のスレッドが同期ブロックに入るべきではないと思っていた場所に、key
の値が同じであるためです。
他の人が指摘しているintern()
ように、そのような目的で使用し、それらの文字列を同期することは、実際には悪い考えであることが判明しました.webappに対してJMeterテストを実行して予想される負荷をシミュレートすると、使用されるヒープサイズが20分弱。
現在、メソッド全体を同期するだけの単純なソリューションを使用していますが、martinprobst と MBCook が提供するコード サンプルが非常に気に入っていますが、現在、このクラスには約 7 つの同様のメソッドgetData()
があるため (約 7 つの異なるデータが必要なため)ロックの取得と解放に関するほとんど重複したロジックを各メソッドに追加したくありませんでした。しかし、これは間違いなく、将来の使用にとって非常に価値のある情報です。これらは、このようなスレッドセーフな操作を行うための最善の方法に関する最終的な正解だと思います。できれば、これらの回答にもっと投票したいと思います!