Java の String オブジェクトの同期
-
02-07-2019 - |
質問
現在、負荷/パフォーマンス テストを行っている Web アプリがあります。特に、数百人のユーザーが同じページにアクセスし、このページで約 10 秒ごとに更新を押すことが予想される機能についてです。この関数でできる改善点の 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
有効期限が切れたので、キャッシュの 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 操作を効果的に同期しているかどうかを判断できるようにしました。
しかし、これはうまくいかなかったようです。私のテストログには次のような出力があります。
(ログ出力は「スレッド名」「ロガー名」「メッセージ」です)
http-80-Processor253 jsp.view-page - getSomeDataForEmail:同期ブロックに入ろうとしています
http-80-Processor253 jsp.view-page - getSomeDataForEmail:同期ブロック内
http-80-Processor253 キャッシュ.StaticCache - 取得:キー [SomeData-test@test.com] のオブジェクトの有効期限が切れています
http-80-Processor253 キャッシュ.StaticCache - 取得:キー [SomeData-test@test.com] 戻り値 [null]
http-80-Processor263 jsp.view-page - getSomeDataForEmail:同期ブロックに入ろうとしています
http-80-Processor263 jsp.view-page - getSomeDataForEmail:同期ブロック内
http-80-Processor263 キャッシュ.StaticCache - 取得:キー [SomeData-test@test.com] のオブジェクトの有効期限が切れています
http-80-Processor263 キャッシュ.StaticCache - 取得:キー [SomeData-test@test.com] 戻り値 [null]
http-80-Processor131 jsp.view-page - getSomeDataForEmail:同期ブロックに入ろうとしています
http-80-Processor131 jsp.view-page - getSomeDataForEmail:同期ブロック内
http-80-Processor131 キャッシュ.StaticCache - 取得:キー [SomeData-test@test.com] のオブジェクトの有効期限が切れています
http-80-Processor131 キャッシュ.StaticCache - 取得:キー [SomeData-test@test.com] 戻り値 [null]
http-80-Processor104 jsp.view-page - getSomeDataForEmail:同期ブロック内
http-80-Processor104 キャッシュ.StaticCache - 取得:キー [SomeData-test@test.com] のオブジェクトの有効期限が切れています
http-80-Processor104 キャッシュ.StaticCache - 取得:キー [SomeData-test@test.com] 戻り値 [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
メソッド内で宣言されているため、各スレッドがへの参照を取得すると考えていました。 同じオブジェクト したがって、この単一のオブジェクトに対して同期が行われます。
ここで私は何を間違っているのでしょうか?
アップデート:さらにログを調べてみると、キーが常に同じである同じ同期ロジックを備えたメソッドのようです。
final String key = "blah";
...
synchronized(key) { ...
同じ同時実行性の問題は発生しません。一度に 1 つのスレッドのみがブロックに入ります。
アップデート 2:助けてくれたみんなに感謝します!についての最初の回答を受け入れました intern()
Strings を使用すると、最初の問題が解決されました。つまり、複数のスレッドが、入れるべきではないと思っていた同期ブロックに入っていたのです。 key
は同じ値でした。
他の人が指摘したように、使用すると intern()
このような目的で使用しており、これらの文字列で同期するのは実際には悪い考えであることが判明しました。予想される負荷をシミュレートするために Web アプリケーションに対して JMeter テストを実行すると、使用されるヒープ サイズが 20 分足らずでほぼ 1 GB に増加することがわかりました。
現在、私はメソッド全体を同期するだけの単純な解決策を使用していますが、 本当に martinprobst と MBCook が提供するコード サンプルのようなものですが、類似したものが 7 つほどあるので、 getData()
現在、このクラスにはメソッドが含まれているため (Web サービスから約 7 つの異なるデータが必要なため)、ロックの取得と解放に関するほぼ重複したロジックを各メソッドに追加したくありませんでした。しかし、これは間違いなく、将来の使用にとって非常に貴重な情報です。これらは、このような操作をスレッドセーフにする最善の方法に関する最終的な正しい答えであると思います。できれば、これらの答えにもっと票を投じたいと思います。
解決
頭をフル回転させずに、あなたの発言をざっと読んだところ、文字列を intern() する必要があるように見えます。
final String firstkey = "Data-" + email;
final String key = firstkey.intern();
それ以外の場合、同じ値を持つ 2 つの String は必ずしも同じオブジェクトであるとは限りません。
VM の奥深くで intern() がロックを取得する必要がある可能性があるため、これにより新たな競合点が生じる可能性があることに注意してください。この分野で最新の VM がどのようなものであるかはわかりませんが、恐ろしいほど最適化されていると期待されます。
StaticCache は依然としてスレッドセーフである必要があることはご存知かと思います。ただし、getSomeDataForEmail の呼び出し中にキーだけではなくキャッシュをロックした場合に発生する競合と比較すると、競合は小さいはずです。
質問に対する回答の更新:
それは文字列リテラルが常に同じオブジェクトを生成するためだと思います。Dave Costa は、それよりもさらに優れているとコメントで指摘しています。リテラルは常に正規表現を生成します。したがって、プログラム内の任意の場所に同じ値を持つすべての String リテラルは、同じオブジェクトを生成します。
編集
他の人も指摘していますが、 インターン文字列で同期するのは実際には非常に悪い考えです - インターン文字列の作成が許可されているため、インターン文字列を永続的に存在させることができます。また、プログラム内の複数のコードがインターン文字列と同期している場合、それらのコード間に依存関係があり、デッドロックやその他のバグが発生しないためです。不可能かもしれない。
キー文字列ごとにロックオブジェクトを保存することでこれを回避する戦略は、私が入力している他の回答で開発されています。
ここに代替案があります。これでも単一のロックが使用されていますが、いずれにせよキャッシュにそのうちの 1 つが必要になることはわかっています。また、5000 スレッドではなく 50 スレッドについて話しているので、致命的ではないかもしれません。また、ここでのパフォーマンスのボトルネックは、DoSlowThing() での I/O のブロッキングが遅く、シリアル化されないことで大きなメリットが得られることだと考えています。それがボトルネックではない場合は、次のようにします。
- CPU がビジーな場合、このアプローチでは不十分な可能性があるため、別のアプローチが必要になります。
- CPU がビジーでなく、サーバーへのアクセスがボトルネックではない場合、このアプローチは過剰です。このアプローチとキーごとのロックの両方を忘れて、操作全体に大きな同期 (静的キャッシュ) を配置して、次のようにした方がよいでしょう。それは簡単な方法です。
明らかに、このアプローチは使用前にスケーラビリティについてソーク テストを行う必要があります。私は何も保証しません。
このコードでは、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 ダミー、および実行に時間がかかる操作に適切な抽象化を定義すると、このコードを複数のキャッシュで使用するために共通化できます。すべてをキャッシュ上のメソッドにまとめることは悪い考えではないかもしれません。
他のヒント
インターン化された String で同期することは、まったく良い考えではないかもしれません。インターン化すると、String はグローバル オブジェクトに変わります。アプリケーションの異なる部分で同じインターン化された文字列を同期すると、非常に奇妙になり、デッドロックなど、基本的にはデバッグ不可能な同期の問題。ありそうもないことのように思えるかもしれませんが、実際にそうなってしまうと本当に大変なことになります。原則として、モジュール外部のコードがロックする可能性がないことが確実なローカル オブジェクトでのみ同期してください。
あなたの場合、同期されたハッシュテーブルを使用して、キーのロックオブジェクトを保存できます。
例えば。:
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 が含まれるかどうかによって決まります。
また:
- 願っています 静的キャッシュ.get() そして セット() メソッドはスレッドセーフです。
- String.intern() コストがかかるため (VM の実装によって異なります)、使用には注意が必要です。
他の人は文字列をインターンすることを提案していますが、それはうまくいきます。
問題は、Java がインターンされた文字列を保持しなければならないことです。次回誰かがその文字列を使用するときに値が同じである必要があるため、参照を保持していない場合でもこれが行われると言われました。これは、すべての文字列をインターンするとメモリを消費し始める可能性があることを意味し、あなたが説明している負荷では大きな問題になる可能性があります。
これに対する 2 つの解決策を見てきました。
別のオブジェクトで同期することもできます
電子メールの代わりに、電子メールの値を変数として保持する電子メールを保持するオブジェクト (User オブジェクトなど) を作成します。その人を表す別のオブジェクトがすでにある場合 (電子メールに基づいて 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 マップに含まれないようにするための対策が必要です。使用する LRUマップApache Commons の 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 クラスを変更することもできるかもしれません。
次のような適切なキャッシュ フレームワークを使用します。 ehcache.
優れたキャッシュを実装することは、一部の人が信じているほど簡単ではありません。
String.intern() がメモリ リークの原因であるというコメントについては、実際にはそうではありません。インターンされた文字列 は ガベージ コレクションの場合、特定の JVM (SUN) ではフル GC のみがアクセスする Perm 領域に保存されるため、さらに時間がかかる可能性があります。
以下は、同期用の専用ロック オブジェクトのマップを使用する、安全で短い 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;
}
しかし、その後、人気のあるキーはマップに常に再挿入され、ロック オブジェクトが再割り当てされることになります。
アップデート:また、これにより、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
スレッドセーフであり、異なるキーに対する同時読み取りと書き込みの影響を受けません。
主な問題は、同じ値を持つ String のインスタンスが複数存在する可能性があるということだけではありません。主な問題は、StaticCache オブジェクトにアクセスするために同期するモニターが 1 つしか必要ないことです。そうしないと、複数のスレッドが (キーが異なる場合でも) StaticCache を同時に変更することになる可能性があり、これはおそらく同時変更をサポートしていません。
呼び出し:
final String key = "Data-" + email;
メソッドが呼び出されるたびに新しいオブジェクトを作成します。このオブジェクトはロックに使用するものであり、このメソッドを呼び出すたびに新しいオブジェクトが作成されるため、実際にはキーに基づいてマップへのアクセスを同期しているわけではありません。
これにより、編集内容がさらに詳しく説明されます。静的な文字列がある場合、それは機能します。
intern() を使用すると、String クラスによって保持されている内部プールから文字列が返されるため、問題は解決されます。これにより、2 つの文字列が等しい場合には、プール内の文字列が使用されます。見る
http://java.sun.com/j2se/1.4.2/docs/api/java/lang/String.html#intern()
この質問は少し広すぎるように思えます。そのため、同様に広範な回答が集まりました。それで答えてみます 質問 からリダイレクトされましたが、残念ながら、その 1 つは重複として閉じられました。
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);
}
かなり遅くなりましたが、ここには間違ったコードが多数含まれています。
この例では:
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 は put 値が必要かどうかを知ることができないため)。または、委任を慎重に使用する必要があります (Future を使用するなど)。機能しますが、多少不一致です。以下を参照)、プット値は必要に応じてオンデマンドで取得されます。
Future の使用は可能ですが、かなりぎこちなく、おそらく少し過剰設計であるように思えます。Future API は、非同期操作、特にすぐには完了しない可能性のある操作の中核です。Future を関与させると、おそらくスレッド作成の層が追加され、おそらく不要な複雑さが追加されます。
このタイプの操作に Future を使用する場合の主な問題は、Future が本質的にマルチスレッドと結びついていることです。新しいスレッドが必要ない場合に Future を使用すると、Future の機構の多くが無視されることになり、この用途では API が過度に重くなります。
ユーザーに提供され、x 分ごとに再生成される静的 HTML ページをレンダリングするだけではどうでしょうか?
また、文字列連結が必要ない場合は、文字列連結を完全に削除することをお勧めします。
final String key = "Data-" + email;
キャッシュ内に、キーの先頭に追加の「Data-」が必要な電子メール アドレスを使用する他のオブジェクト/タイプはありますか?
そうでないなら、私はそれを作ります
final String key = email;
また、余分な文字列の作成も回避できます。
他の方法で文字列オブジェクトを同期します。
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
}
}
}
他の人が同様の問題を抱えている場合に備えて、私の知る限り、次のコードは機能します。
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;
});
}
同期されたコードから何も返さない場合は、synchronize メソッドを次のように記述できます。
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);
}
}
}
}
文字列を含む任意のキーをロック/同期できる小さなロック クラスを追加しました。
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 {プライベート最終的な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();
}
}
}
あなたの場合、次のようなものを使用できます(これはメモリをリークしません)。
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'
文字列値がシステム全体で一意であることが合理的に保証できる場合は、同期に String.intern を安全に使用できます。UUIDS はこれにアプローチする良い方法です。キャッシュやマップを介して、UUID を実際の文字列キーに関連付けたり、エンティティ オブジェクトのフィールドとして UUID を保存したりすることもできます。
@Service
public class MySyncService{
public Map<String, String> lockMap=new HashMap<String, String>();
public void syncMethod(String email) {
String lock = lockMap.get(email);
if(lock==null) {
lock = UUID.randomUUID().toString();
lockMap.put(email, lock);
}
synchronized(lock.intern()) {
//do your sync code here
}
}