9

次の特性を持つメモ化キャッシュを作成しています。

  • キャッシュミスは、エントリの計算と保存につながります
    • この計算は非常に高価です
    • この計算はべき等です
  • 制限なし(エントリは削除されません)以降:
    • 入力は最大500エントリになります
    • 保存された各エントリは非常に小さい
    • キャッシュは比較的短命です(通常は1時間未満)
    • 全体として、メモリ使用量は問題ではありません
  • 何千もの読み取りがあります-キャッシュの存続期間にわたって、99.9%以上のキャッシュヒットが予想されます
  • スレッドセーフである必要があります

何が優れたパフォーマンスを発揮するのでしょうか、またはどのような条件下で一方のソリューションが他方よりも優先されるのでしょうか。

ThreadLocal HashMap:

class MyCache {
    private static class LocalMyCache {
        final Map<K,V> map = new HashMap<K,V>();

        V get(K key) {
            V val = map.get(key);
            if (val == null) {
                val = computeVal(key);
                map.put(key, val);
            }
            return val;
        }
    }

    private final ThreadLocal<LocalMyCache> localCaches = new ThreadLocal<LocalMyCache>() {
        protected LocalMyCache initialValue() {
            return new LocalMyCache();
        }
    };

    public V get(K key) {
        return localCaches.get().get(key);
    }
}

ConcurrentHashMap:

class MyCache {
    private final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<K,V>();

    public V get(K key) {
        V val = map.get(key);
        if (val == null) {
            val = computeVal(key);
            map.put(key, val);
        }
        return val;
    }
}

スレッドごとのすべてのキャッシュミスのためにスレッドが多い場合、ThreadLocalソリューションは最初は遅くなると思いますが、数千回の読み取りを超えると、償却コストはConcurrentHashMapソリューションよりも低くなります。私の直感は正しいですか?

または、さらに良い解決策はありますか?

4

6 に答える 6

4

キャッシュはお勧めできませんので、ThreadLocalを使用してください

ほとんどのコンテナでは、スレッドはスレッドプールを介して再利用されるため、gcになることはありません。これは有線の何かにつながるでしょう

Memリークを防ぐために管理する必要があるConcurrentHashMapを使用してください

あなたが主張するなら、私は週またはソフトrefを使用してリッチmaxsizeの後に立ち退きすることをお勧めします

memキャッシュソリューションを見つけた場合(車輪の再発明はしないでください)、guavaキャッシュを試してください http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/cache/CacheBuilder.html

于 2013-01-12T15:37:09.940 に答える
3

この計算は非常に高価です

これがあなたがキャッシュを作成した理由であり、これがあなたの主な関心事であると思います。

ソリューションの速度はわずかに異なる場合があります<<100nsですが、スレッド間で結果を共有できることがより重要だと思います。つまり、ConcurrentHashMapは、長期的にはより多くのCPU時間を節約できる可能性があるため、アプリケーションに最適である可能性があります。

つまり、ソリューションの速度は、同じものを複数回計算するコストと比較して小さい可能性があります(複数のスレッドの場合)

于 2013-01-12T15:49:36.157 に答える
2

ConcurrentHashMapの実装はスレッドセーフではなく、1つのアイテムが2回計算される可能性があることに注意してください。明示的なロックを使用せずに結果を直接保存する場合、実際にはそれを正しく行うのは非常に複雑です。パフォーマンスが懸念される場合は、これを避けたいと思います。

ConcurrentHashMapは非常にスケーラブルであり、高い競合の下でうまく機能することは注目に値します。ThreadLocalのパフォーマンスが向上するかどうかはわかりません。

ライブラリを使用する以外に、実践リスト5.19のJava並行性からインスピレーションを得ることができます。Future<V>アイデアは、の代わりにマップにを保存することVです。これは、効率を維持しながら(ロックフリー)、メソッド全体のスレッドを安全にするのに大いに役立ちます。参照用に以下の実装を貼り付けますが、すべての詳細が重要であることを理解するために、この章を読む価値があります。

public interface Computable<K, V> {

    V compute(K arg) throws InterruptedException;
}

public class Memoizer<K, V> implements Computable<K, V> {

    private final ConcurrentMap<K, Future<V>> cache = new ConcurrentHashMap<K, Future<V>>();
    private final Computable<K, V> c;

    public Memoizer(Computable<K, V> c) {
        this.c = c;
    }

    public V compute(final K arg) throws InterruptedException {
        while (true) {
            Future<V> f = cache.get(arg);
            if (f == null) {
                Callable<V> eval = new Callable<V>() {
                    public V call() throws InterruptedException {
                        return c.compute(arg);
                    }
                };
                FutureTask<V> ft = new FutureTask<V>(eval);
                f = cache.putIfAbsent(arg, ft);
                if (f == null) {
                    f = ft;
                    ft.run();
                }
            }
            try {
                return f.get();
            } catch (CancellationException e) {
                cache.remove(arg, f);
            } catch (ExecutionException e) {
                throw new RuntimeException(e.getCause());
            }
        }
    }
}
于 2013-01-12T17:12:09.480 に答える
1

これらの両方を実装するのは比較的簡単なので、両方を試して定常状態の負荷でテストし、どちらがアプリケーションに最適かを確認することをお勧めします。

私の推測では、のようConcurrentHashMapにネイティブ呼び出しを行う必要がないため、は少し速くなります。ただし、これは、保存しているオブジェクトとそのハッシュコーディングの効率によって異なる場合があります。Thread.currentThread()ThreadLocal

concurrencyLevel並行マップを必要なスレッド数に調整することも価値があるかもしれません。デフォルトは16です。

于 2013-01-12T15:42:07.640 に答える
1

ルックアップ速度は、おそらく両方のソリューションで同じです。マルチスレッドの問題に対する最善の解決策はシングルスレッドであるため、他に懸念がない場合は、ThreadLocalをお勧めします。

ただし、主な問題は、同じキーの同時計算を望まないことです。したがって、キーごとにロックが必要です。このようなロックは通常、ConcurrentHashMapによって実装できます。

だから私の解決策は

class LazyValue
{
    K key;

    volatile V value;

    V getValue() {  lazy calculation, doubled-checked locking }
}


static ConcurrentHashMap<K, LazyValue> centralMap = ...;
static
{
    for every key
        centralMap.put( key, new LazyValue(key) );
}


static V lookup(K key)
{
    V value = localMap.get(key);
    if(value==null)
        localMap.put(key, value=centralMap.get(key).getValue())
    return value;
}
于 2013-01-12T16:32:39.613 に答える
1

ソリューションは同等ではないため、パフォーマンスの質問は関係ありません。

ThreadLocalハッシュマップはスレッド間で共有されないため、スレッドセーフの問題は発生しませんが、各スレッドが独自のキャッシュを持っていることについては何も述べていないという仕様も満たしていません。

スレッドセーフの要件は、単一のキャッシュがすべてのスレッド間で共有されることを意味し、これによりThreadLocalが完全に除外されます。

于 2013-01-13T00:18:35.670 に答える