web-dev-qa-db-ja.com

java内の1つのオブジェクトのスレッドセーフキャッシュ

アプリケーションにCountryListオブジェクトがあり、国のリストを返すとします。国のロードは負荷の大きい操作なので、リストはキャッシュする必要があります。

追加要件:

  • CountryListはスレッドセーフである必要があります
  • CountryListは遅延ロードする必要があります(オンデマンドのみ)
  • CountryListはキャッシュの無効化をサポートする必要があります
  • キャッシュがほとんど無効にならないことを考慮して、CountryListを最適化する必要があります

私は次の解決策を思いつきました:

public class CountryList {
    private static final Object ONE = new Integer(1);

    // MapMaker is from Google Collections Library    
    private Map<Object, List<String>> cache = new MapMaker()
        .initialCapacity(1)
        .makeComputingMap(
            new Function<Object, List<String>>() {
                @Override
                public List<String> apply(Object from) {
                    return loadCountryList();
                }
            });

    private List<String> loadCountryList() {
        // HEAVY OPERATION TO LOAD DATA
    }

    public List<String> list() {
        return cache.get(ONE);
    }

    public void invalidateCache() {
        cache.remove(ONE);
    }
}

あなたはそれについてどう思いますか?それについて何か悪いと思いますか?それを行う他の方法はありますか?どうすれば改善できますか?この場合、完全に別の解決策を探す必要がありますか?

ありがとう。

37
Igor Mukhin

すべての人に感謝します、特に、ユーザー「gid」に考え。

私の目標は、invalidate()操作が非常にまれに呼び出されることを考慮して、get()操作のパフォーマンスを最適化することでした。

16個のスレッドを開始するテストクラスを作成し、それぞれが100万回get()-Operationを呼び出しました。このクラスを使用して、2コアのマシンにいくつかの実装をプロファイルしました。

テスト結果

Implementation              Time
no synchronisation          0,6 sec
normal synchronisation      7,5 sec
with MapMaker               26,3 sec
with Suppliers.memoize      8,2 sec
with optimized memoize      1,5 sec

1)「同期なし」はスレッドセーフではありませんが、比較できる最高のパフォーマンスを提供します。

@Override
public List<String> list() {
    if (cache == null) {
        cache = loadCountryList();
    }
    return cache;
}

@Override
public void invalidateCache() {
    cache = null;
}

2)「通常の同期」-かなり良いパフォーマンス、標準的な簡単な実装

@Override
public synchronized List<String> list() {
    if (cache == null) {
        cache = loadCountryList();
    }
    return cache;
}

@Override
public synchronized void invalidateCache() {
    cache = null;
}

3)「MapMakerを使用」-パフォーマンスが非常に低い。

コードについては、上の質問を参照してください。

4) "with Suppliers.memoize"-良いパフォーマンス。ただし、パフォーマンスは同じ「通常の同期」であるため、最適化するか、「通常の同期」を使用する必要があります。

コードについては、ユーザー「gid」の回答を参照してください。

5) "最適化されたメモ化"-"同期なし"の実装に匹敵するパフォーマンスですが、スレッドセーフなものです。これが必要です。

Cache-class自体:(ここで使用されるサプライヤーインターフェースはGoogleコレクションライブラリからのもので、get()メソッドが1つだけあります。参照 http://google-collections.googlecode.com/svn/trunk/javadoc/ com/google/common/base/Supplier.html

public class LazyCache<T> implements Supplier<T> {
    private final Supplier<T> supplier;

    private volatile Supplier<T> cache;

    public LazyCache(Supplier<T> supplier) {
        this.supplier = supplier;
        reset();
    }

    private void reset() {
        cache = new MemoizingSupplier<T>(supplier);
    }

    @Override
    public T get() {
        return cache.get();
    }

    public void invalidate() {
        reset();
    }

    private static class MemoizingSupplier<T> implements Supplier<T> {
        final Supplier<T> delegate;
        volatile T value;

        MemoizingSupplier(Supplier<T> delegate) {
            this.delegate = delegate;
        }

        @Override
        public T get() {
            if (value == null) {
                synchronized (this) {
                    if (value == null) {
                        value = delegate.get();
                    }
                }
            }
            return value;
        }
    }
}

使用例:

public class BetterMemoizeCountryList implements ICountryList {

    LazyCache<List<String>> cache = new LazyCache<List<String>>(new Supplier<List<String>>(){
        @Override
        public List<String> get() {
            return loadCountryList();
        }
    });

    @Override
    public List<String> list(){
        return cache.get();
    }

    @Override
    public void invalidateCache(){
        cache.invalidate();
    }

    private List<String> loadCountryList() {
        // this should normally load a full list from the database,
        // but just for this instance we mock it with:
        return Arrays.asList("Germany", "Russia", "China");
    }
}
17
Igor Mukhin

グーグルコレクションは実際にこの種のもののためだけのものを提供します: サプライヤー

あなたのコードは次のようになります:

private Supplier<List<String>> supplier = new Supplier<List<String>>(){
    public List<String> get(){
        return loadCountryList();
    }
};


// volatile reference so that changes are published correctly see invalidate()
private volatile Supplier<List<String>> memorized = Suppliers.memoize(supplier);


public List<String> list(){
    return memorized.get();
}

public void invalidate(){
    memorized = Suppliers.memoize(supplier);
}
33
Gareth Davis

何かをキャッシュする必要があるときはいつでも、 プロキシパターン を使用します。このパターンでそれを行うと、懸念が分離されます。元のオブジェクトは遅延読み込みに関係する可能性があります。プロキシ(またはガーディアン)オブジェクトがキャッシュの検証を担当できます。

詳細に:

  • できれば同期ブロックまたはその他の セマフォ ロックを使用して、スレッドセーフなオブジェクトCountryListクラスを定義します。
  • このクラスのインターフェースをCountryQueryableインターフェースに抽出します。
  • CountryQueryableを実装する別のオブジェクトCountryListProxyを定義します。
  • CountryListProxyのインスタンス化のみを許可し、そのインターフェースを通じての参照のみを許可します。

ここから、キャッシュ無効化戦略をプロキシオブジェクトに挿入できます。最後のロードの時間を保存し、データを表示する次のリクエスト時に、現在の時間をキャッシュ時間と比較します。許容レベルを定義します。時間が過ぎると、データが再ロードされます。

Lazy Loadについては、 こちら を参照してください。

次に、いくつかの優れたダウンサンプルコードについて説明します。

public interface CountryQueryable {

    public void operationA();
    public String operationB();

}

public class CountryList implements CountryQueryable {

    private boolean loaded;

    public CountryList() {
        loaded = false;
    }

    //This particular operation might be able to function without
    //the extra loading.
    @Override
    public void operationA() {
        //Do whatever.
    }

    //This operation may need to load the extra stuff.
    @Override
    public String operationB() {
        if (!loaded) {
            load();
            loaded = true;
        }

        //Do whatever.
        return whatever;
    }

    private void load() {
        //Do the loading of the Lazy load here.
    }

}

public class CountryListProxy implements CountryQueryable {

    //In accordance with the Proxy pattern, we hide the target
    //instance inside of our Proxy instance.
    private CountryQueryable actualList;
    //Keep track of the lazy time we cached.
    private long lastCached;

    //Define a tolerance time, 2000 milliseconds, before refreshing
    //the cache.
    private static final long TOLERANCE = 2000L;

    public CountryListProxy() {
            //You might even retrieve this object from a Registry.
        actualList = new CountryList();
        //Initialize it to something stupid.
        lastCached = Long.MIN_VALUE;
    }

    @Override
    public synchronized void operationA() {
        if ((System.getCurrentTimeMillis() - lastCached) > TOLERANCE) {
            //Refresh the cache.
                    lastCached = System.getCurrentTimeMillis();
        } else {
            //Cache is okay.
        }
    }

    @Override
    public synchronized String operationB() {
        if ((System.getCurrentTimeMillis() - lastCached) > TOLERANCE) {
            //Refresh the cache.
                    lastCached = System.getCurrentTimeMillis();
        } else {
            //Cache is okay.
        }

        return whatever;
    }

}

public class Client {

    public static void main(String[] args) {
        CountryQueryable queryable = new CountryListProxy();
        //Do your thing.
    }

}
5
Mike

マップの目的がわかりません。レイジーでキャッシュされたオブジェクトが必要な場合、通常は次のようにします。

public class CountryList
{
  private static List<Country> countryList;

  public static synchronized List<Country> get()
  {
    if (countryList==null)
      countryList=load();
    return countryList;
  }
  private static List<Country> load()
  {
    ... whatever ...
  }
  public static synchronized void forget()
  {
    countryList=null;
  }
}

これはあなたがやっていることに似ていると思いますが、もう少し簡単です。地図と、質問のために簡略化したONEが必要な場合は、それで構いません。

スレッドセーフにする場合は、getとforgetを同期させる必要があります。

1
Jay

あなたはそれについてどう思いますか?それについて何か悪いと思いますか?

Bleah-単一の機能が必要なため、複数の機能(マップアクセス、同時実行に適したアクセス、値の遅延構築など)を備えた複雑なデータ構造MapMakerを使用しています(単一の構築コストの高いオブジェクトの作成の遅延) 。

コードの再利用は良い目標ですが、このアプローチはオーバーヘッドと複雑さを追加します。さらに、マップデータ構造がそこにあるときに、実際に1つしか存在しない場合(国のリスト)にキー/値のマップがあると考えて、将来のメンテナを誤解させます。シンプルさ、読みやすさ、明快さは、将来の保守性の鍵です。

それを行う他の方法はありますか?どうすれば改善できますか?この場合、完全に別の解決策を探すべきですか?

遅延読み込みを行ったようです。他のSO遅延読み込みの質問の解決策を見てください。たとえば、これは古典的なダブルチェックのアプローチをカバーしています(Java 1.5または後で):

Javaで「ダブルチェックされたロックが壊れている」宣言を解決する方法

ここでソリューションコードを単に繰り返すのではなく、ナレッジベースを拡大するために、ダブルチェックによる遅延読み込みに関する説明を読むことは有用だと思います。 (それが壮大なものとなる場合は申し訳ありません-何とか何とか何とか何も食べないで魚を教えることを試みるだけです...)

1
Bert F

そこにライブラリがあります(from atlassian )- LazyReference と呼ばれるユーティリティクラスの1つ。 LazyReferenceは、遅延して(最初の取得時に)作成できるオブジェクトへの参照です。これはスレッドセーフであり、initも1回だけ発生することが保証されています。2つのスレッドが同時にget()を呼び出すと、1つのスレッドが計算し、もう1つのスレッドは待機をブロックします。

サンプルコードを参照

final LazyReference<MyObject> ref = new LazyReference() {
    protected MyObject create() throws Exception {
        // Do some useful object construction here
        return new MyObject();
    }
};

//thread1
MyObject myObject = ref.get();
//thread2
MyObject myObject = ref.get();
1
Chii

ここでは、あなたのニーズはかなり単純に見えます。 MapMakerを使用すると、実装が必要以上に複雑になります。ダブルチェックされたロッキングイディオム全体を正しく理解するにはトリッキーであり、1.5以上でのみ機能します。そして正直なところ、これはプログラミングの最も重要なルールの1つを破っています。

時期尚早の最適化は、すべての悪の根源です。

ダブルチェックされたロッキングイディオムは、キャッシュがすでにロードされている場合の同期のコストを回避しようとします。しかし、そのオーバーヘッドは本当に問題を引き起こしていますか?より複雑なコードのコストに見合う価値がありますか?私は、プロファイリングがあなたに別のことを言うまでそうではないと仮定します。

以下は、サードパーティのコードを必要としない非常にシンプルなソリューションです(JCIPアノテーションは無視)。空のリストは、キャッシュがまだロードされていないことを意味すると想定しています。また、国リストの内容が、返されたリストを変更する可能性のあるクライアントコードにエスケープするのを防ぎます。これが問題にならない場合は、Collections.unmodifiedList()の呼び出しを削除できます。

public class CountryList {

    @GuardedBy("cache")
    private final List<String> cache = new ArrayList<String>();

    private List<String> loadCountryList() {
        // HEAVY OPERATION TO LOAD DATA
    }

    public List<String> list() {
        synchronized (cache) {
            if( cache.isEmpty() ) {
                cache.addAll(loadCountryList());
            }
            return Collections.unmodifiableList(cache);
        }
    }

    public void invalidateCache() {
        synchronized (cache) {
            cache.clear();
        }
    }

}
1
wolfcastle

これは私には問題ありません(MapMakerはgoogleコレクションからのものだと思いますか?)理想的には、実際にはキーがないのでマップを使用する必要はありませんが、実装は呼び出し元から隠されているため、これは大したこと。

0
Mike Q

上記のマイクのソリューションをフォローアップします。コメントが期待どおりにフォーマットされませんでした... :(

特にload()が遅いため、operationBでの同期の問題に注意してください。

public String operationB() {
    if (!loaded) {
        load();
        loaded = true;
    }

    //Do whatever.
    return whatever;
}

次のように修正できます。

public String operationB() {
    synchronized(loaded) {
        if (!loaded) {
            load();
            loaded = true;
        }
    }

    //Do whatever.
    return whatever;
}

ロードされた変数へのすべてのアクセスで必ず同期するようにしてください。

0
romacafe

これは、ComputingMapを簡単に使用する方法です。すべてのメソッドが同期されている完全にシンプルな実装のみが必要であり、問​​題はありません。これにより、最初のスレッドがヒットする(取得する)と、最初のスレッドがキャッシュをロードしている間にヒットする他のすべてのスレッドがブロックされます(誰かがinvalidateCacheを呼び出した場合も同じです-invalidateCacheが新たにキャッシュするか、またはそれをnullにして、再度取得しようとする最初の試行をブロックします)が、すべてのスレッドは適切に通過します。

0
stolsvik

オンデマンドホルダーイディオムでの初期化 を使用します。

public class CountryList {
  private CountryList() {}

  private static class CountryListHolder {
    static final List<Country> INSTANCE = new List<Country>();
  }

  public static List<Country> getInstance() {
    return CountryListHolder.INSTANCE;
  }

  ...
}
0
helpermethod