54

特に、数百人のユーザーが同じページにアクセスし、このページで約 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 つの異なるデータが必要なため)ロックの取得と解放に関するほとんど重複したロジックを各メソッドに追加したくありませんでした。しかし、これは間違いなく、将来の使用にとって非常に価値のある情報です。これらは、このようなスレッドセーフな操作を行うための最善の方法に関する最終的な正解だと思います。できれば、これらの回答にもっと投票したいと思います!

4

20 に答える 20

44

私の脳を完全に準備することなく、あなたの言うことを簡単にスキャンすると、文字列をインターン()する必要があるように見えます:

final String firstkey = "Data-" + email;
final String key = firstkey.intern();

それ以外の場合、同じ値を持つ 2 つの文字列は必ずしも同じオブジェクトではありません。

VM の奥深くで、intern() がロックを取得する必要がある場合があるため、これにより新しい競合点が生じる可能性があることに注意してください。この分野で最新の VM がどのように見えるかはわかりませんが、非常に最適化されていることを願っています。

StaticCache は依然としてスレッドセーフである必要があることをご存知だと思います。しかし、getSomeDataForEmail の呼び出し中にキーだけでなくキャッシュをロックしていた場合に比べれば、競合は小さいはずです。

質問の更新への回答:

これは、文字列リテラルが常に同じオブジェクトを生成するためだと思います。Dave Costa は、それよりも優れているとコメントで指摘しています。リテラルは常に正規表現を生成します。したがって、プログラム内のどこでも同じ値を持つすべての文字列リテラルは、同じオブジェクトを生成します。

編集

他の人は、インターン文字列で同期することは実際には非常に悪い考えだと指摘しています.1つは、インターン文字列を作成するとそれらが永続的に存在することを許可されているためであり、もう1つは、プログラム内のどこかのコードの複数のビットがインターン文字列で同期している場合です.これらのコード間に依存関係があり、デッドロックやその他のバグを防ぐことは不可能な場合があります。

キー文字列ごとにロックオブジェクトを保存することでこれを回避する戦略は、入力時に他の回答で開発されています。

別の方法があります - それはまだ単一のロックを使用していますが、とにかくキャッシュ用にそれらの 1 つが必要になることはわかっています.5000 ではなく 50 スレッドについて話していたので、致命的ではないかもしれません. また、ここでのパフォーマンスのボトルネックは、DoSlowThing() で I/O をブロックするのが遅いことであると想定しています。したがって、シリアル化しないことで大きなメリットが得られます。それがボトルネックでない場合は、次のようになります。

  • CPU がビジー状態の場合、この方法では不十分である可能性があり、別の方法が必要になります。
  • CPU がビジーではなく、サーバーへのアクセスがボトルネックでない場合、このアプローチはやり過ぎであり、これとキーごとのロックの両方を忘れて、操作全体に大きな同期 (StaticCache) を配置して、それは簡単な方法です。

明らかに、このアプローチは、使用前にスケーラビリティについて十分にテストする必要があります。私は何も保証しません。

このコードでは、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();
        }
    }
}

キャッシュに何かが追加されるたびに、すべてのスレッドがウェイクアップしてキャッシュをチェックするため (どのキーを求めていても)、競合の少ないアルゴリズムでパフォーマンスを向上させることができます。ただし、その作業の多くは、大量のアイドル状態の CPU 時間が I/O をブロックしている間に行われるため、問題にはならない可能性があります。

このコードは、複数のキャッシュで使用するために共通化できます。キャッシュとそれに関連するロック、返されるデータ、IN_PROGRESS ダミー、および実行する遅い操作に対して適切な抽象化を定義する場合です。すべてをキャッシュ上のメソッドにまとめることは、悪い考えではないかもしれません。

于 2008-09-25T15:30:41.650 に答える
28

インターンされた文字列で同期することはまったく良い考えではないかもしれません-それをインターンすることによって、ストリングはグローバルオブジェクトに変わり、アプリケーションの異なる部分で同じインターンされた文字列で同期すると、本当に奇妙になり、デッドロックなど、基本的にデバッグ不可能な同期の問題。ありそうもないように見えるかもしれませんが、それが起こったとき、あなたは本当に困惑しています。原則として、モジュールの外部のコードがオブジェクトをロックしないことが絶対に確実なローカルオブジェクトでのみ同期します。

あなたの場合、同期されたハッシュテーブルを使用して、キーのロックオブジェクトを格納できます。

例えば:

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")全体を同期させることができます。これは、すべてのスレッドがキャッシュにアクセスするときに同期する必要があることを意味します。これは、パフォーマンスの問題になる可能性があります。しかし、いつものように、最初にこの単純な解決策を試して、それが本当に問題であるかどうかを確認してください!多くの場合、同期よりも結果の処理にはるかに多くの時間を費やすため、そうではないはずです。

于 2008-09-25T15:53:37.210 に答える
12

文字列は同期の適切な候補ではありません。文字列IDで同期する必要がある場合は、文字列を使用してミューテックスを作成することで同期できます(「IDでの同期」を参照)。そのアルゴリズムのコストに見合う価値があるかどうかは、サービスの呼び出しに重要なI/Oが含まれるかどうかによって異なります。

また:

  • StaticCache.get( )メソッドとset()メソッドがスレッドセーフであることを願っています。
  • String.intern()にはコストがかかるため(VMの実装によって異なります)、注意して使用する必要があります。
于 2008-09-25T16:17:38.197 に答える
6

他の人は文字列をインターンすることを提案しました、そしてそれはうまくいくでしょう。

問題は、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マップに表示されないようにするための何かが必要になります。Apache CommonsのLRUMapを使用すると、それが可能になります。

これには多少の調整が必要ですが、問題が解決する場合があります。

別のキーを使用する

起こりうるエラーに我慢したい場合(これがどれほど重要かはわかりません)、文字列のハッシュコードをキーとして使用できます。intをインターンする必要はありません。

概要

これがお役に立てば幸いです。スレッディングは楽しいですね。また、セッションを使用して「これを見つけるためにすでに作業中です」という意味の値を設定し、2番目(3番目、N番目)のスレッドが作成を試みる必要があるかどうか、または結果が表示されるのを待つ必要があるかどうかを確認することもできますキャッシュ内。私は3つの提案があったと思います。

于 2008-09-25T16:04:40.070 に答える
5

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 クラスを変更するための基礎として使用できます。

于 2008-09-27T17:26:01.847 に答える
4

ehcacheなどの適切なキャッシングフレームワークを使用します。

優れたキャッシュの実装は、一部の人々が信じているほど簡単ではありません。

String.intern()がメモリリークの原因であるというコメントに関しては、実際にはそうではありません。インターンされた文字列ガベージコレクションされます。特定のJVM(SUN)では、完全なGCによってのみアクセスされるPermスペースに格納されるため、時間がかかる場合があります。

于 2008-10-08T08:48:10.257 に答える
2

呼び出し:

   final String key = "Data-" + email;

メソッドが呼び出されるたびに新しいオブジェクトを作成します。そのオブジェクトはロックに使用するものであり、このメソッドを呼び出すたびに新しいオブジェクトが作成されるため、キーに基づいてマップへのアクセスを実際には同期していません。

これはあなたの編集をさらに説明します。静的文字列がある場合、それは機能します。

intern() を使用すると、String クラスによって保持される内部プールから文字列が返されるため、問題が解決されます。これにより、2 つの文字列が等しい場合、プール内の文字列が使用されることが保証されます。見る

http://java.sun.com/j2se/1.4.2/docs/api/java/lang/String.html#intern()

于 2008-09-25T15:39:07.557 に答える
2

あなたの主な問題は、同じ値を持つ String の複数のインスタンスが存在する可能性があるということだけではありません。主な問題は、StaticCache オブジェクトにアクセスするために同期するモニターが 1 つしか必要ないことです。そうしないと、複数のスレッドが StaticCache を同時に変更してしまう可能性があり (異なるキーの下ではありますが)、これはおそらく同時変更をサポートしていません。

于 2008-09-25T15:39:28.570 に答える
2

この質問は私には少し広すぎるように思われるため、同じように幅広い回答のセットを扇動しました. そのため、リダイレクトされた質問に答えようとしますが、残念ながら、その質問は重複として閉じられています。

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操作では、(内部) ロックが取得され、マップへの排他的アクセスが短時間取得されます。対応するオブジェクトが既にマップ内にある場合、現在のスレッドは待機し、そうでない場合は newConditionをマップし、(内部) ロックを解放して続行すると、(外部) ロックが取得されたと見なされます。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);
    }
于 2018-05-11T18:31:56.307 に答える
1

これはかなり遅れていますが、ここにはかなり多くの間違ったコードが示されています。

この例では:

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 を使用します。これは機能しますが、多少の不一致があります。以下を参照してください)。必要に応じてオンデマンドで put 値が取得されます。

Futures の使用は可能ですが、かなりぎこちなく、おそらく少しオーバーエンジニアリングのようです。Future API は、非同期操作、特に、すぐには完了しない可能性がある操作の中核です。Future を巻き込むと、おそらくスレッド作成のレイヤーが追加されます。余分な、おそらく不必要な複雑さです。

このタイプの操作に Future を使用する際の主な問題は、Future が本質的にマルチスレッドと結び付いていることです。新しいスレッドが必要ない場合に Future を使用すると、Future の多くの機構が無視され、この用途では非常に重い API になります。

于 2013-08-22T19:13:46.057 に答える
0

ユーザーに提供され、x 分ごとに再生成される静的な html ページをレンダリングしないのはなぜですか?

于 2008-09-25T15:59:34.013 に答える
0

また、文字列の連結が不要な場合は、完全に削除することをお勧めします。

final String key = "Data-" + email;

キーの先頭に余分な「Data-」が必要な電子メールアドレスを使用するキャッシュ内の他のもの/オブジェクトのタイプはありますか?

そうでなければ、私はそれを作るだろう

final String key = email;

また、余分な文字列の作成もすべて回避します。

于 2008-09-25T19:06:59.477 に答える
0

有効期間が短いオブジェクトを同期で使用する場合は、細心の注意を払う必要があります。すべての Java オブジェクトにはモニターが接続されており、デフォルトでは、このモニターは収縮されています。ただし、2 つのスレッドがモニターの取得で競合すると、モニターが膨張します。オブジェクトの寿命が長い場合、これは問題ではありません。ただし、オブジェクトの寿命が短い場合、この膨張したモニターをクリーンアップすると、GC 時間に深刻な打撃を与える可能性があります (そのため、レイテンシが高くなり、スループットが低下します)。また、GC 時間は常にリストされているとは限らないため、GC 時間を特定するのも難しい場合があります。

同期したい場合は、java.util.concurrent.Lock を使用できます。または、手動で作成されたストライプ ロックを利用し、文字列のハッシュをそのストライプ ロックのインデックスとして使用します。GC の問題が発生しないように、このストライプ ロックを保持します。

だから、このようなもの:

static final Object[] locks = newLockArray();

Object lock = locks[hashToIndex(key.hashcode(),locks.length];
synchronized(lock){
       ....
}

int hashToIndex(int hash, int length) {
    if (hash == Integer.MIN_VALUE return 0;
    return abs(hash) % length;
}
于 2020-10-07T07:48:16.620 に答える