0

私は最近、通常のオブジェクトに対して二重にバッファリングされたスレッドセーフキャッシュを実装する方法を探していました。

リクエストごとに何度もヒットし、非常に大きなドキュメントからキャッシュからリロードする必要があるキャッシュされたデータ構造があり(1秒以上のアンマーシャリング時間)、それによってすべてのリクエストを遅延させる余裕がなかったため、必要が生じました。毎分長い。

良いスレッドセーフな実装を見つけることができなかったので、私は自分で書いたのですが、それが正しいかどうか、そしてそれを小さくすることができるかどうか疑問に思っています...ここにあります:

package nl.trimpe.michiel

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Abstract class implementing a double buffered cache for a single object.
 * 
 * Implementing classes can load the object to be cached by implementing the
 * {@link #retrieve()} method.
 * 
 * @param <T>
 *            The type of the object to be cached.
 */
public abstract class DoublyBufferedCache<T> {

    private static final Log log = LogFactory.getLog(DoublyBufferedCache.class);

    private Long timeToLive;

    private long lastRetrieval;

    private T cachedObject;

    private Object lock = new Object();

    private volatile Boolean isLoading = false;

    public T getCachedObject() {
        checkForReload();
        return cachedObject;
    }

    private void checkForReload() {
        if (cachedObject == null || isExpired()) {
            if (!isReloading()) {
                synchronized (lock) {
                    // Recheck expiration because another thread might have
                    // refreshed the cache before we were allowed into the
                    // synchronized block.
                    if (isExpired()) {
                        isLoading = true;
                        try {
                            cachedObject = retrieve();
                            lastRetrieval = System.currentTimeMillis();
                        } catch (Exception e) {
                            log.error("Exception occurred retrieving cached object", e);
                        } finally {
                            isLoading = false;
                        }
                    }
                }
            }
        }
    }

    protected abstract T retrieve() throws Exception;

    private boolean isExpired() {
        return (timeToLive > 0) ? ((System.currentTimeMillis() - lastRetrieval) > (timeToLive * 1000)) : true;
    }

    private boolean isReloading() {
        return cachedObject != null && isLoading;
    }

    public void setTimeToLive(Long timeToLive) {
        this.timeToLive = timeToLive;
    }

}
4

4 に答える 4

3

あなたが書いたものはスレッドセーフではありません。実際、あなたは非常に有名な問題である一般的な誤謬に出くわしました。これはダブルチェックロック問題と呼ばれ、あなたのような多くの解決策(そしてこのテーマにはいくつかのバリエーションがあります)にはすべて問題があります。

これにはいくつかの解決策がありますが、最も簡単なのは、ScheduledThreadExecutorServiceを使用して、必要なものを1分ごとに、または頻繁にリロードすることです。リロードすると、それがキャッシュ結果に入れられ、それを呼び出すと最新バージョンが返されます。これはスレッドセーフで、実装が簡単です。オンデマンドでロードされていないことは確かですが、初期値を除いて、値を取得するときにパフォーマンスが低下することはありません。これを、遅延読み込みではなく、過度に熱心な読み込みと呼びます。

例えば:

public class Cache<T> {
  private final ScheduledExecutorsService executor =
    Executors.newSingleThreadExecutorService();
  private final Callable<T> method;
  private final Runnable refresh;
  private Future<T> result;
  private final long ttl;

  public Cache(Callable<T> method, long ttl) {
    if (method == null) {
      throw new NullPointerException("method cannot be null");
    }
    if (ttl <= 0) {
      throw new IllegalArgumentException("ttl must be positive");
    }
    this.method = method;
    this.ttl = ttl;

    // initial hits may result in a delay until we've loaded
    // the result once, after which there will never be another
    // delay because we will only refresh with complete results
    result = executor.submit(method);

    // schedule the refresh process
    refresh = new Runnable() {
      public void run() {
        Future<T> future = executor.submit(method);
        future.get();
        result = future;
        executor.schedule(refresh, ttl, TimeUnit.MILLISECONDS);
      }
    }
    executor.schedule(refresh, ttl, TimeUnit.MILLISECONDS);
  }

  public T getResult() {
    return result.get();
  }
}

それには少し説明が必要です。基本的に、Callableの結果をキャッシュするための汎用インターフェイスを作成しています。これがドキュメントのロードになります。Callable(またはRunnable)を送信すると、Futureが返されます。Future.get()を呼び出すと、返される(完了する)までブロックされます。

したがって、これは、Futureの観点からget()メソッドを実装して、最初のクエリが失敗しないようにします(ブロックされます)。その後、'ttl'ミリ秒ごとにrefreshメソッドが呼び出されます。メソッドをスケジューラーに送信し、Future.get()を呼び出します。これにより、結果が生成され、結果が完了するのを待ちます。完了すると、「result」メンバーが置き換えられます。サブシーケンスCache.get()呼び出しは、新しい値を返します。

ScheduledExecutorServiceにはscheduleWithFixedRate()メソッドがありますが、Callableがスケジュールされた遅延よりも長くかかると、同時に複数の実行が発生し、それまたはスロットルについて心配する必要があるため、回避します。更新の最後にプロセスが自分自身を送信するだけの方が簡単です。

于 2009-09-14T13:49:21.977 に答える
0

必要なのが最初のロード時間ではなく、リロードである場合、リロードの実際の時間を気にしないかもしれませんが、新しいバージョンをロードするときに古いバージョンを使用できるようにしたいですか?

それが必要な場合は、フィールドで使用可能なインスタンス(静的ではなく)をキャッシュにすることをお勧めします。

  1. 専用スレッド(または少なくとも通常のスレッドではない)を使用して毎分リロードをトリガーし、通常のスレッドを遅らせないようにします。

  2. 再読み込みすると、新しいインスタンスが作成され、データが読み込まれ(1秒かかります)、古いインスタンスが新しいインスタンスに置き換えられます。(古いものはガベージコレクションされます。)オブジェクトを別のオブジェクトに置き換えることは不可分操作です。

分析:その場合、他のスレッドは最後の瞬間まで古いキャッシュにアクセスできますか?
最悪の場合、古いキャッシュインスタンスを取得した直後の命令で、別のスレッドが古いインスタンスを新しいインスタンスに置き換えます。ただし、これによってコードに問題が生じることはありません。古いキャッシュインスタンスに問い合わせても、直前に正しい値が得られます。これは、最初の文として示した要件で受け入れられます。

コードをより正確にするために、キャッシュインスタンスを不変として作成できます(使用可能なセッターがなく、内部状態を変更する方法もありません)。これにより、マルチスレッドのコンテキストで使用することが正しいことが明確になります。

于 2009-09-14T14:15:48.707 に答える
0

私はあなたの必要性を理解しているかどうかわかりません。値の一部について、キャッシュのロード(およびリロード)を高速化する必要がありますか?

もしそうなら、私はあなたのデータ構造をより小さな断片に分割することを提案します。その時に必要なピースをロードするだけです。サイズを10で割ると、読み込み時間を10に関連するもので割ることになります。

これは、可能であれば、読んでいる元のドキュメントに当てはまる可能性があります。そうでなければ、それはあなたがそれを読む方法であり、あなたはそれの大部分をスキップして、関連する部分だけをロードします。

ほとんどのデータは細かく分割できると思います。より適切なものを選択してください。例を次に示します。

  • 開始文字:A *、B*..。
  • IDを2つの部分に分割します。最初の部分はカテゴリであり、キャッシュでそれを探し、必要に応じてロードしてから、内部で2番目の部分を探します。
于 2009-09-14T13:53:11.760 に答える
-1

あなたは必要以上にロックしているように見えます。あなたの良いケース(キャッシュがいっぱいで有効)では、すべてのリクエストがロックを取得します。キャッシュの有効期限が切れている場合にのみ、ロックを解除できます。

リロードする場合は、何もしません。
リロードしない場合は、有効期限が切れていないかどうかを確認してください。リロードしておらず、有効期限が切れている場合は、ロックとダブルチェックの有効期限を取得して、最後のチェックから正常にロードされていないことを確認します。

また、バックグラウンドスレッドでキャッシュをリロードして、キャッシュがいっぱいになるのを待っている1つの必要条件が保留されないようにすることもできます。


    private void checkForReload() {
        if (cachedObject == null || isExpired()) {
                if (!isReloading()) {

                       // Recheck expiration because another thread might have
                       // refreshed the cache before we were allowed into the
                        // synchronized block.
                        if (isExpired()) {
                                synchronized (lock) {
                                        if (isExpired()) {
                                                isLoading = true;
                                                try {
                                                        cachedObject = retrieve();
                                                        lastRetrieval = System.currentTimeMillis();
                                                } catch (Exception e) {
                                                        log.error("Exception occurred retrieving cached object", e);
                                                } finally {
                                                        isLoading = false;
                                                }
                                        }
                                }
                        }
                }
        }

于 2009-09-14T14:17:13.933 に答える