172

EHCacheやOSCacheなどとは言わないでください。この質問の目的のために、SDKだけを使用して自分で実装したいとします(実行して学習します)。キャッシュがマルチスレッド環境で使用されるとすると、どのデータ構造を使用しますか?LinkedHashMapCollections#synchronizedMapを使用して既に実装しましたが、新しい同時コレクションのいずれかがより適切な候補になるかどうか知りたいです。

更新:このナゲットを見つけたとき、私はYeggeの最新情報を読んでいました:

一定時間のアクセスが必要で、挿入順序を維持したい場合は、本当に素晴らしいデータ構造であるLinkedHashMapよりも優れた方法はありません。それがおそらくもっと素晴らしいかもしれない唯一の方法は、同時バージョンがあったかどうかです。しかし悲しいかな。

上記のLinkedHashMap+実装を使用する前は、ほぼ同じことを考えていました。Collections#synchronizedMap私が何かを見落としていたのではないことを知ってうれしいです。

これまでの回答に基づくと、並行性の高いLRUに対する私の最善の策は、を使用するのと同じロジックのいくつかを使用してConcurrentHashMapLinkedHashMapを拡張することであるように思われます。

4

21 に答える 21

105

LinkedHashMap私はこれらの提案の多くを気に入っていますが、今のところ+に固執すると思いますCollections.synchronizedMap。将来これを再訪する場合は、おそらくextendsConcurrentHashMapと同じ方法で拡張することに取り組みます。LinkedHashMapHashMap

アップデート:

リクエストにより、これが私の現在の実装の要点です。

private class LruCache<A, B> extends LinkedHashMap<A, B> {
    private final int maxEntries;

    public LruCache(final int maxEntries) {
        super(maxEntries + 1, 1.0f, true);
        this.maxEntries = maxEntries;
    }

    /**
     * Returns <tt>true</tt> if this <code>LruCache</code> has more entries than the maximum specified when it was
     * created.
     *
     * <p>
     * This method <em>does not</em> modify the underlying <code>Map</code>; it relies on the implementation of
     * <code>LinkedHashMap</code> to do that, but that behavior is documented in the JavaDoc for
     * <code>LinkedHashMap</code>.
     * </p>
     *
     * @param eldest
     *            the <code>Entry</code> in question; this implementation doesn't care what it is, since the
     *            implementation is only dependent on the size of the cache
     * @return <tt>true</tt> if the oldest
     * @see java.util.LinkedHashMap#removeEldestEntry(Map.Entry)
     */
    @Override
    protected boolean removeEldestEntry(final Map.Entry<A, B> eldest) {
        return super.size() > maxEntries;
    }
}

Map<String, String> example = Collections.synchronizedMap(new LruCache<String, String>(CACHE_SIZE));
于 2009-12-23T15:43:23.770 に答える
19

今日これをゼロからやり直すとしたら、Guava のCacheBuilder.

于 2012-12-13T02:21:07.740 に答える
10

これはラウンド 2 です。

最初のラウンドは私が思いついたものであり、ドメインがもう少し頭に染み込んでいるコメントを読み直しました。

したがって、これは、他のいくつかのバージョンに基づいて動作することを示す単体テストを備えた最も単純なバージョンです。

最初の非並行バージョン:

import java.util.LinkedHashMap;
import java.util.Map;

public class LruSimpleCache<K, V> implements LruCache <K, V>{

    Map<K, V> map = new LinkedHashMap (  );


    public LruSimpleCache (final int limit) {
           map = new LinkedHashMap <K, V> (16, 0.75f, true) {
               @Override
               protected boolean removeEldestEntry(final Map.Entry<K, V> eldest) {
                   return super.size() > limit;
               }
           };
    }
    @Override
    public void put ( K key, V value ) {
        map.put ( key, value );
    }

    @Override
    public V get ( K key ) {
        return map.get(key);
    }

    //For testing only
    @Override
    public V getSilent ( K key ) {
        V value =  map.get ( key );
        if (value!=null) {
            map.remove ( key );
            map.put(key, value);
        }
        return value;
    }

    @Override
    public void remove ( K key ) {
        map.remove ( key );
    }

    @Override
    public int size () {
        return map.size ();
    }

    public String toString() {
        return map.toString ();
    }


}

true フラグは、gets と puts のアクセスを追跡します。JavaDocs を参照してください。true フラグをコンストラクターに持たない removeEdelstEntry は、FIFO キャッシュを実装するだけです (FIFO と removeEldestEntry に関する以下の注を参照してください)。

LRU キャッシュとして機能することを証明するテストは次のとおりです。

public class LruSimpleTest {

    @Test
    public void test () {
        LruCache <Integer, Integer> cache = new LruSimpleCache<> ( 4 );


        cache.put ( 0, 0 );
        cache.put ( 1, 1 );

        cache.put ( 2, 2 );
        cache.put ( 3, 3 );


        boolean ok = cache.size () == 4 || die ( "size" + cache.size () );


        cache.put ( 4, 4 );
        cache.put ( 5, 5 );
        ok |= cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 2 ) == 2 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();
        ok |= cache.getSilent ( 4 ) == 4 || die ();
        ok |= cache.getSilent ( 5 ) == 5 || die ();


        cache.get ( 2 );
        cache.get ( 3 );
        cache.put ( 6, 6 );
        cache.put ( 7, 7 );
        ok |= cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 2 ) == 2 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();
        ok |= cache.getSilent ( 4 ) == null || die ();
        ok |= cache.getSilent ( 5 ) == null || die ();


        if ( !ok ) die ();

    }

さて、同時バージョンについて...

パッケージ org.boon.cache;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LruSimpleConcurrentCache<K, V> implements LruCache<K, V> {

    final CacheMap<K, V>[] cacheRegions;


    private static class CacheMap<K, V> extends LinkedHashMap<K, V> {
        private final ReadWriteLock readWriteLock;
        private final int limit;

        CacheMap ( final int limit, boolean fair ) {
            super ( 16, 0.75f, true );
            this.limit = limit;
            readWriteLock = new ReentrantReadWriteLock ( fair );

        }

        protected boolean removeEldestEntry ( final Map.Entry<K, V> eldest ) {
            return super.size () > limit;
        }


        @Override
        public V put ( K key, V value ) {
            readWriteLock.writeLock ().lock ();

            V old;
            try {

                old = super.put ( key, value );
            } finally {
                readWriteLock.writeLock ().unlock ();
            }
            return old;

        }


        @Override
        public V get ( Object key ) {
            readWriteLock.writeLock ().lock ();
            V value;

            try {

                value = super.get ( key );
            } finally {
                readWriteLock.writeLock ().unlock ();
            }
            return value;
        }

        @Override
        public V remove ( Object key ) {

            readWriteLock.writeLock ().lock ();
            V value;

            try {

                value = super.remove ( key );
            } finally {
                readWriteLock.writeLock ().unlock ();
            }
            return value;

        }

        public V getSilent ( K key ) {
            readWriteLock.writeLock ().lock ();

            V value;

            try {

                value = this.get ( key );
                if ( value != null ) {
                    this.remove ( key );
                    this.put ( key, value );
                }
            } finally {
                readWriteLock.writeLock ().unlock ();
            }
            return value;

        }

        public int size () {
            readWriteLock.readLock ().lock ();
            int size = -1;
            try {
                size = super.size ();
            } finally {
                readWriteLock.readLock ().unlock ();
            }
            return size;
        }

        public String toString () {
            readWriteLock.readLock ().lock ();
            String str;
            try {
                str = super.toString ();
            } finally {
                readWriteLock.readLock ().unlock ();
            }
            return str;
        }


    }

    public LruSimpleConcurrentCache ( final int limit, boolean fair ) {
        int cores = Runtime.getRuntime ().availableProcessors ();
        int stripeSize = cores < 2 ? 4 : cores * 2;
        cacheRegions = new CacheMap[ stripeSize ];
        for ( int index = 0; index < cacheRegions.length; index++ ) {
            cacheRegions[ index ] = new CacheMap<> ( limit / cacheRegions.length, fair );
        }
    }

    public LruSimpleConcurrentCache ( final int concurrency, final int limit, boolean fair ) {

        cacheRegions = new CacheMap[ concurrency ];
        for ( int index = 0; index < cacheRegions.length; index++ ) {
            cacheRegions[ index ] = new CacheMap<> ( limit / cacheRegions.length, fair );
        }
    }

    private int stripeIndex ( K key ) {
        int hashCode = key.hashCode () * 31;
        return hashCode % ( cacheRegions.length );
    }

    private CacheMap<K, V> map ( K key ) {
        return cacheRegions[ stripeIndex ( key ) ];
    }

    @Override
    public void put ( K key, V value ) {

        map ( key ).put ( key, value );
    }

    @Override
    public V get ( K key ) {
        return map ( key ).get ( key );
    }

    //For testing only
    @Override
    public V getSilent ( K key ) {
        return map ( key ).getSilent ( key );

    }

    @Override
    public void remove ( K key ) {
        map ( key ).remove ( key );
    }

    @Override
    public int size () {
        int size = 0;
        for ( CacheMap<K, V> cache : cacheRegions ) {
            size += cache.size ();
        }
        return size;
    }

    public String toString () {

        StringBuilder builder = new StringBuilder ();
        for ( CacheMap<K, V> cache : cacheRegions ) {
            builder.append ( cache.toString () ).append ( '\n' );
        }

        return builder.toString ();
    }


}

非並行バージョンを最初に取り上げる理由がわかります。上記は、ロックの競合を減らすためにいくつかのストライプを作成しようとしています。そのため、キーをハッシュし、そのハッシュを検索して実際のキャッシュを見つけます。これにより、制限サイズは、キー ハッシュ アルゴリズムがどれだけ分散されているかに応じて、かなりの量のエラー内でより多くの提案/大まかな推測になります。

これは、並行バージョンがおそらく機能することを示すテストです。:)(火の下でのテストが本当の方法です)。

public class SimpleConcurrentLRUCache {


    @Test
    public void test () {
        LruCache <Integer, Integer> cache = new LruSimpleConcurrentCache<> ( 1, 4, false );


        cache.put ( 0, 0 );
        cache.put ( 1, 1 );

        cache.put ( 2, 2 );
        cache.put ( 3, 3 );


        boolean ok = cache.size () == 4 || die ( "size" + cache.size () );


        cache.put ( 4, 4 );
        cache.put ( 5, 5 );

        puts (cache);
        ok |= cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 2 ) == 2 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();
        ok |= cache.getSilent ( 4 ) == 4 || die ();
        ok |= cache.getSilent ( 5 ) == 5 || die ();


        cache.get ( 2 );
        cache.get ( 3 );
        cache.put ( 6, 6 );
        cache.put ( 7, 7 );
        ok |= cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 2 ) == 2 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();

        cache.put ( 8, 8 );
        cache.put ( 9, 9 );

        ok |= cache.getSilent ( 4 ) == null || die ();
        ok |= cache.getSilent ( 5 ) == null || die ();


        puts (cache);


        if ( !ok ) die ();

    }


    @Test
    public void test2 () {
        LruCache <Integer, Integer> cache = new LruSimpleConcurrentCache<> ( 400, false );


        cache.put ( 0, 0 );
        cache.put ( 1, 1 );

        cache.put ( 2, 2 );
        cache.put ( 3, 3 );


        for (int index =0 ; index < 5_000; index++) {
            cache.get(0);
            cache.get ( 1 );
            cache.put ( 2, index  );
            cache.put ( 3, index );
            cache.put(index, index);
        }

        boolean ok = cache.getSilent ( 0 ) == 0 || die ();
        ok |= cache.getSilent ( 1 ) == 1 || die ();
        ok |= cache.getSilent ( 2 ) != null || die ();
        ok |= cache.getSilent ( 3 ) != null || die ();

        ok |= cache.size () < 600 || die();
        if ( !ok ) die ();



    }

}

これが最後の投稿です。LRU キャッシュではなく LFU だったので削除した最初の投稿です。

私はこれをもう一度やってみようと思いました。私は、実装が多すぎる標準のJDKを使用して、LRUキャッシュの最も単純なバージョンを考え出そうとしていました。

これが私が思いついたものです。私の最初の試みは、LRU の代わりに LFU を実装し、FIFO と LRU サポートを追加したため、少し惨事でした...そして、それが怪物になりつつあることに気付きました。それから、ほとんど興味を示さなかった友人のジョンと話し始め、LFU、LRU、FIFO をどのように実装したか、そして単純な ENUM 引数でそれを切り替える方法を詳しく説明しました。単純な LRU でした。ですから、私からの以前の投稿は無視して、列挙型を介して切り替え可能な LRU/LFU/FIFO キャッシュを見たいかどうか教えてください...いいえ? わかりました.. どうぞ。

JDK のみを使用した最も単純な LRU。並行バージョンと非並行バージョンの両方を実装しました。

私は共通のインターフェースを作成しました(これはミニマリズムなので、あなたが望むいくつかの機能が欠けている可能性がありますが、私のユースケースでは機能しますが、XYZの機能を見たい場合はお知らせください...私はコードを書くために生きています。) .

public interface LruCache<KEY, VALUE> {
    void put ( KEY key, VALUE value );

    VALUE get ( KEY key );

    VALUE getSilent ( KEY key );

    void remove ( KEY key );

    int size ();
}

getSilentとは何か疑問に思うかもしれません。これをテストに使用します。getSilent はアイテムの LRU スコアを変更しません。

まず非同時のもの....

import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

public class LruCacheNormal<KEY, VALUE> implements LruCache<KEY,VALUE> {

    Map<KEY, VALUE> map = new HashMap<> ();
    Deque<KEY> queue = new LinkedList<> ();
    final int limit;


    public LruCacheNormal ( int limit ) {
        this.limit = limit;
    }

    public void put ( KEY key, VALUE value ) {
        VALUE oldValue = map.put ( key, value );

        /*If there was already an object under this key,
         then remove it before adding to queue
         Frequently used keys will be at the top so the search could be fast.
         */
        if ( oldValue != null ) {
            queue.removeFirstOccurrence ( key );
        }
        queue.addFirst ( key );

        if ( map.size () > limit ) {
            final KEY removedKey = queue.removeLast ();
            map.remove ( removedKey );
        }

    }


    public VALUE get ( KEY key ) {

        /* Frequently used keys will be at the top so the search could be fast.*/
        queue.removeFirstOccurrence ( key );
        queue.addFirst ( key );
        return map.get ( key );
    }


    public VALUE getSilent ( KEY key ) {

        return map.get ( key );
    }

    public void remove ( KEY key ) {

        /* Frequently used keys will be at the top so the search could be fast.*/
        queue.removeFirstOccurrence ( key );
        map.remove ( key );
    }

    public int size () {
        return map.size ();
    }

    public String toString() {
        return map.toString ();
    }
}

大きなキャッシュがある場合、queue.removeFirstOccurrence はコストがかかる可能性がある操作です例として LinkedList を取り上げ、要素からノードへの逆ルックアップ ハッシュ マップを追加して、削除操作をより高速で一貫性のあるものにすることができます。私も始めましたが、それが必要ないことに気付きました。だけど、たぶん...

putが呼び出されると、キーがキューに追加されます。getが呼び出されると、キーが削除され、キューの先頭に再度追加されます。

キャッシュが小さく、アイテムの構築に費用がかかる場合、これは適切なキャッシュになるはずです。キャッシュが非常に大きい場合、特にキャッシュのホット エリアがない場合は、線形検索がボトルネックになる可能性があります。ホット スポットが激しいほど、ホット アイテムは常に線形検索の最上位にあるため、線形検索は高速になります。とにかく...これを高速化するために必要なのは、削除のためのノードルックアップへの逆要素を持つ削除操作を持つ別のLinkedListを作成することです。削除は、ハッシュマップからキーを削除するのと同じくらい高速です。

1,000 アイテム未満のキャッシュがある場合、これでうまくいくはずです。

これは、実際の操作を示す簡単なテストです。

public class LruCacheTest {

    @Test
    public void test () {
        LruCache<Integer, Integer> cache = new LruCacheNormal<> ( 4 );


        cache.put ( 0, 0 );
        cache.put ( 1, 1 );

        cache.put ( 2, 2 );
        cache.put ( 3, 3 );


        boolean ok = cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 0 ) == 0 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();


        cache.put ( 4, 4 );
        cache.put ( 5, 5 );
        ok |= cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 0 ) == null || die ();
        ok |= cache.getSilent ( 1 ) == null || die ();
        ok |= cache.getSilent ( 2 ) == 2 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();
        ok |= cache.getSilent ( 4 ) == 4 || die ();
        ok |= cache.getSilent ( 5 ) == 5 || die ();

        if ( !ok ) die ();

    }
}

最後の LRU キャッシュはシングルスレッドでした。同期化されたものにラップしないでください....

これは、並行バージョンでの刺し傷です。

import java.util.Deque;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

public class ConcurrentLruCache<KEY, VALUE> implements LruCache<KEY,VALUE> {

    private final ReentrantLock lock = new ReentrantLock ();


    private final Map<KEY, VALUE> map = new ConcurrentHashMap<> ();
    private final Deque<KEY> queue = new LinkedList<> ();
    private final int limit;


    public ConcurrentLruCache ( int limit ) {
        this.limit = limit;
    }

    @Override
    public void put ( KEY key, VALUE value ) {
        VALUE oldValue = map.put ( key, value );
        if ( oldValue != null ) {
            removeThenAddKey ( key );
        } else {
            addKey ( key );
        }
        if (map.size () > limit) {
            map.remove ( removeLast() );
        }
    }


    @Override
    public VALUE get ( KEY key ) {
        removeThenAddKey ( key );
        return map.get ( key );
    }


    private void addKey(KEY key) {
        lock.lock ();
        try {
            queue.addFirst ( key );
        } finally {
            lock.unlock ();
        }


    }

    private KEY removeLast( ) {
        lock.lock ();
        try {
            final KEY removedKey = queue.removeLast ();
            return removedKey;
        } finally {
            lock.unlock ();
        }
    }

    private void removeThenAddKey(KEY key) {
        lock.lock ();
        try {
            queue.removeFirstOccurrence ( key );
            queue.addFirst ( key );
        } finally {
            lock.unlock ();
        }

    }

    private void removeFirstOccurrence(KEY key) {
        lock.lock ();
        try {
            queue.removeFirstOccurrence ( key );
        } finally {
            lock.unlock ();
        }

    }


    @Override
    public VALUE getSilent ( KEY key ) {
        return map.get ( key );
    }

    @Override
    public void remove ( KEY key ) {
        removeFirstOccurrence ( key );
        map.remove ( key );
    }

    @Override
    public int size () {
        return map.size ();
    }

    public String toString () {
        return map.toString ();
    }
}

主な違いは、HashMap の代わりに ConcurrentHashMap を使用することと、Lock を使用することです (同期化を回避できたかもしれませんが...)。

炎上してテストしたことはありませんが、単純な LRU マップが必要なユースケースの 80% でうまくいく可能性がある単純な LRU キャッシュのようです。

ライブラリ a、b、または c を使用しない理由を除いて、フィードバックを歓迎します。私が常にライブラリを使用するとは限らない理由は、すべての war ファイルを常に 80MB にしたいわけではないためです。 - 必要に応じて、別のキャッシュ プロバイダーで。:) いつ誰かが Guava や ehcache など、含めたくないものを必要とするかはわかりませんが、キャッシングをプラグ可能にすれば、それらも除外しません。

依存関係の削減には、独自の報酬があります。これをさらに簡単または高速にする方法、またはその両方を行う方法についてフィードバックをいただけると幸いです。

また、誰かが行く準備ができていることを知っていれば....

わかりました..あなたが考えていることはわかります...なぜ彼はLinkedHashMapからremoveEldestエントリを使用しないのですか?私はそうすべきですが....しかし..しかし..それはLRUではなくFIFOになり、私たちはLRU を実装しようとしています。

    Map<KEY, VALUE> map = new LinkedHashMap<KEY, VALUE> () {

        @Override
        protected boolean removeEldestEntry ( Map.Entry<KEY, VALUE> eldest ) {
            return this.size () > limit;
        }
    };

上記のコードでは、このテストは失敗します...

        cache.get ( 2 );
        cache.get ( 3 );
        cache.put ( 6, 6 );
        cache.put ( 7, 7 );
        ok |= cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 2 ) == 2 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();
        ok |= cache.getSilent ( 4 ) == null || die ();
        ok |= cache.getSilent ( 5 ) == null || die ();

これは、removeEldestEntry を使用した簡単で汚れた FIFO キャッシュです。

import java.util.*;

public class FifoCache<KEY, VALUE> implements LruCache<KEY,VALUE> {

    final int limit;

    Map<KEY, VALUE> map = new LinkedHashMap<KEY, VALUE> () {

        @Override
        protected boolean removeEldestEntry ( Map.Entry<KEY, VALUE> eldest ) {
            return this.size () > limit;
        }
    };


    public LruCacheNormal ( int limit ) {
        this.limit = limit;
    }

    public void put ( KEY key, VALUE value ) {
         map.put ( key, value );


    }


    public VALUE get ( KEY key ) {

        return map.get ( key );
    }


    public VALUE getSilent ( KEY key ) {

        return map.get ( key );
    }

    public void remove ( KEY key ) {
        map.remove ( key );
    }

    public int size () {
        return map.size ();
    }

    public String toString() {
        return map.toString ();
    }
}

FIFO は高速です。周りを探す必要はありません。LRU の前に FIFO を配置すると、ほとんどのホット エントリを適切に処理できます。より優れた LRU には、逆要素からノードへの機能が必要になります。

とにかく...コードを書いたので、他の回答を見て、見逃したものを見てみましょう...初めてスキャンしたとき。

于 2013-12-17T21:23:57.503 に答える
9

LinkedHashMapO(1)ですが、同期が必要です。そこで車輪の再発明をする必要はありません。

並行性を高めるための2つのオプション:

1.複数LinkedHashMapを作成し、それらにハッシュします。例:LinkedHashMap[4], index 0, 1, 2, 3。キーでdo key%4 (またはbinary ORon [key, 3])を実行して、put / get/removeを実行するマップを選択します。

ConcurrentHashMap2.を拡張し、その中の各領域に構造のようなリンクされたハッシュマップを設定することで、「ほぼ」LRUを実行できます。LinkedHashMapロックは、同期されているものよりもきめ細かく発生します。リストの先頭と末尾のロックが必要です(リージョンごと)putputIfAbsent削除または取得時に、リージョン全体をロックする必要があります。ある種のAtomicリンクリストがここで役立つかどうか知りたいのですが、おそらくリストの先頭に役立つでしょう。多分もっと。

構造は全順序を保持するのではなく、地域ごとの順序のみを保持します。エントリの数がリージョンの数よりもはるかに多い限り、これはほとんどのキャッシュに十分です。各リージョンには独自のエントリカウントが必要です。これは、エビクショントリガーのグローバルカウントではなく使用されます。aのデフォルトのリージョン数ConcurrentHashMapは16で、今日のほとんどのサーバーには十分です。

  1. 適度な並行性の下では、記述がより簡単になり、より高速になります。

  2. 書くのはもっと難しいでしょうが、非常に高い同時実行性ではるかにうまくスケーリングします。通常のアクセスでは遅くなります(同時実行性がないConcurrentHashMap場合よりも遅くなります)HashMap

于 2009-03-05T03:33:34.193 に答える
8

2 つのオープン ソース実装があります。

Apache Solr には ConcurrentLRUCache があります: https://lucene.apache.org/solr/3_6_1/org/apache/solr/util/ConcurrentLRUCache.html

ConcurrentLinkedHashMap のオープン ソース プロジェクトがあります: http://code.google.com/p/concurrentlinkedhashmap/

于 2010-07-16T23:53:03.493 に答える
7

各要素の「numberOfUses」カウンターによって優先度が決定されるjava.util.concurrent.PriorityBlockingQueueの使用を検討します。「numberOfUses」カウンターは、要素が不変ではないことを意味するため、すべての同期を正しく行うように非常に注意します。

要素オブジェクトは、キャッシュ内のオブジェクトのラッパーになります。

class CacheElement {
    private final Object obj;
    private int numberOfUsers = 0;

    CacheElement(Object obj) {
        this.obj = obj;
    }

    ... etc.
}
于 2008-10-21T11:41:20.730 に答える
2

これは私が使用するLRUキャッシュであり、LinkedHashMapをカプセル化し、ジューシーなスポットを保護する単純な同期ロックで同時実行を処理します。使用時に要素に「接触」して、再び「最も新鮮な」要素になるようにします。これにより、実際にはLRUになります。私はまた、最小の寿命を持つ要素の要件を持っていました。これは、許可された「最大のアイドル時間」と考えることもできます。そうすれば、あなたは立ち退きの準備ができています。

しかし、私はハンクの結論に同意し、答えを受け入れました-今日これを再開する場合は、グアバのをチェックしCacheBuilderます。

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;


public class MaxIdleLRUCache<KK, VV> {

    final static private int IDEAL_MAX_CACHE_ENTRIES = 128;

    public interface DeadElementCallback<KK, VV> {
        public void notify(KK key, VV element);
    }

    private Object lock = new Object();
    private long minAge;
    private HashMap<KK, Item<VV>> cache;


    public MaxIdleLRUCache(long minAgeMilliseconds) {
        this(minAgeMilliseconds, IDEAL_MAX_CACHE_ENTRIES);
    }

    public MaxIdleLRUCache(long minAgeMilliseconds, int idealMaxCacheEntries) {
        this(minAgeMilliseconds, idealMaxCacheEntries, null);
    }

    public MaxIdleLRUCache(long minAgeMilliseconds, int idealMaxCacheEntries, final DeadElementCallback<KK, VV> callback) {
        this.minAge = minAgeMilliseconds;
        this.cache = new LinkedHashMap<KK, Item<VV>>(IDEAL_MAX_CACHE_ENTRIES + 1, .75F, true) {
            private static final long serialVersionUID = 1L;

            // This method is called just after a new entry has been added
            public boolean removeEldestEntry(Map.Entry<KK, Item<VV>> eldest) {
                // let's see if the oldest entry is old enough to be deleted. We don't actually care about the cache size.
                long age = System.currentTimeMillis() - eldest.getValue().birth;
                if (age > MaxIdleLRUCache.this.minAge) {
                    if ( callback != null ) {
                        callback.notify(eldest.getKey(), eldest.getValue().payload);
                    }
                    return true; // remove it
                }
                return false; // don't remove this element
            }
        };

    }

    public void put(KK key, VV value) {
        synchronized ( lock ) {
//          System.out.println("put->"+key+","+value);
            cache.put(key, new Item<VV>(value));
        }
    }

    public VV get(KK key) {
        synchronized ( lock ) {
//          System.out.println("get->"+key);
            Item<VV> item = getItem(key);
            return item == null ? null : item.payload;
        }
    }

    public VV remove(String key) {
        synchronized ( lock ) {
//          System.out.println("remove->"+key);
            Item<VV> item =  cache.remove(key);
            if ( item != null ) {
                return item.payload;
            } else {
                return null;
            }
        }
    }

    public int size() {
        synchronized ( lock ) {
            return cache.size();
        }
    }

    private Item<VV> getItem(KK key) {
        Item<VV> item = cache.get(key);
        if (item == null) {
            return null;
        }
        item.touch(); // idle the item to reset the timeout threshold
        return item;
    }

    private static class Item<T> {
        long birth;
        T payload;

        Item(T payload) {
            this.birth = System.currentTimeMillis();
            this.payload = payload;
        }

        public void touch() {
            this.birth = System.currentTimeMillis();
        }
    }

}
于 2013-02-21T17:02:40.987 に答える
2

これは、同期ブロックを使用しない、テスト済みの最高パフォーマンスの同時 LRU キャッシュ実装です。

public class ConcurrentLRUCache<Key, Value> {

private final int maxSize;

private ConcurrentHashMap<Key, Value> map;
private ConcurrentLinkedQueue<Key> queue;

public ConcurrentLRUCache(final int maxSize) {
    this.maxSize = maxSize;
    map = new ConcurrentHashMap<Key, Value>(maxSize);
    queue = new ConcurrentLinkedQueue<Key>();
}

/**
 * @param key - may not be null!
 * @param value - may not be null!
 */
public void put(final Key key, final Value value) {
    if (map.containsKey(key)) {
        queue.remove(key); // remove the key from the FIFO queue
    }

    while (queue.size() >= maxSize) {
        Key oldestKey = queue.poll();
        if (null != oldestKey) {
            map.remove(oldestKey);
        }
    }
    queue.add(key);
    map.put(key, value);
}

/**
 * @param key - may not be null!
 * @return the value associated to the given key or null
 */
public Value get(final Key key) {
    return map.get(key);
}

}

于 2011-05-26T11:46:44.963 に答える
2

キャッシュの場合、通常はプロキシ オブジェクト (URL、文字列など) を介してデータの一部を検索するため、インターフェイスに関してはマップが必要になります。しかし、物事を追い出すには、構造のようなキューが必要です。内部的には、Priority-Queue と HashMap の 2 つのデータ構造を維持します。これは、O(1) 時間ですべてを実行できるはずの実装です。

これが私がかなり素早く作り上げたクラスです:

import java.util.HashMap;
import java.util.Map;
public class LRUCache<K, V>
{
    int maxSize;
    int currentSize = 0;

    Map<K, ValueHolder<K, V>> map;
    LinkedList<K> queue;

    public LRUCache(int maxSize)
    {
        this.maxSize = maxSize;
        map = new HashMap<K, ValueHolder<K, V>>();
        queue = new LinkedList<K>();
    }

    private void freeSpace()
    {
        K k = queue.remove();
        map.remove(k);
        currentSize--;
    }

    public void put(K key, V val)
    {
        while(currentSize >= maxSize)
        {
            freeSpace();
        }
        if(map.containsKey(key))
        {//just heat up that item
            get(key);
            return;
        }
        ListNode<K> ln = queue.add(key);
        ValueHolder<K, V> rv = new ValueHolder<K, V>(val, ln);
        map.put(key, rv);       
        currentSize++;
    }

    public V get(K key)
    {
        ValueHolder<K, V> rv = map.get(key);
        if(rv == null) return null;
        queue.remove(rv.queueLocation);
        rv.queueLocation = queue.add(key);//this ensures that each item has only one copy of the key in the queue
        return rv.value;
    }
}

class ListNode<K>
{
    ListNode<K> prev;
    ListNode<K> next;
    K value;
    public ListNode(K v)
    {
        value = v;
        prev = null;
        next = null;
    }
}

class ValueHolder<K,V>
{
    V value;
    ListNode<K> queueLocation;
    public ValueHolder(V value, ListNode<K> ql)
    {
        this.value = value;
        this.queueLocation = ql;
    }
}

class LinkedList<K>
{
    ListNode<K> head = null;
    ListNode<K> tail = null;

    public ListNode<K> add(K v)
    {
        if(head == null)
        {
            assert(tail == null);
            head = tail = new ListNode<K>(v);
        }
        else
        {
            tail.next = new ListNode<K>(v);
            tail.next.prev = tail;
            tail = tail.next;
            if(tail.prev == null)
            {
                tail.prev = head;
                head.next = tail;
            }
        }
        return tail;
    }

    public K remove()
    {
        if(head == null)
            return null;
        K val = head.value;
        if(head.next == null)
        {
            head = null;
            tail = null;
        }
        else
        {
            head = head.next;
            head.prev = null;
        }
        return val;
    }

    public void remove(ListNode<K> ln)
    {
        ListNode<K> prev = ln.prev;
        ListNode<K> next = ln.next;
        if(prev == null)
        {
            head = next;
        }
        else
        {
            prev.next = next;
        }
        if(next == null)
        {
            tail = prev;
        }
        else
        {
            next.prev = prev;
        }       
    }
}

仕組みは次のとおりです。キーはリンクされたリストに格納され、最も古いキーがリストの先頭に配置されます (新しいキーは後ろに移動します)。そのため、何かを「排出」する必要がある場合は、キューの先頭からポップして、そのキーを使用してマップから値を削除します。アイテムが参照されると、マップから ValueHolder を取得し、queuelocation 変数を使用してキュー内の現在の位置からキーを削除し、キューの最後に配置します (最近使用されたもの)。物事を追加することはほとんど同じです。

ここには大量のエラーがあると確信しており、同期を実装していません。ただし、このクラスは、O(1) キャッシュへの追加、O(1) 古いアイテムの削除、およびキャッシュ アイテムの O(1) 検索を提供します。単純な同期 (すべてのパブリック メソッドを同期するだけ) でも、実行時間によるロックの競合はほとんどありません。誰かが巧妙な同期のトリックを持っているなら、私は非常に興味があります. また、マップに関して maxsize 変数を使用して実装できる追加の最適化がいくつかあると確信しています。

于 2008-10-22T02:22:16.630 に答える
1

これが私の短い実装です。批判または改善してください。

package util.collection;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Limited size concurrent cache map implementation.<br/>
 * LRU: Least Recently Used.<br/>
 * If you add a new key-value pair to this cache after the maximum size has been exceeded,
 * the oldest key-value pair will be removed before adding.
 */

public class ConcurrentLRUCache<Key, Value> {

private final int maxSize;
private int currentSize = 0;

private ConcurrentHashMap<Key, Value> map;
private ConcurrentLinkedQueue<Key> queue;

public ConcurrentLRUCache(final int maxSize) {
    this.maxSize = maxSize;
    map = new ConcurrentHashMap<Key, Value>(maxSize);
    queue = new ConcurrentLinkedQueue<Key>();
}

private synchronized void freeSpace() {
    Key key = queue.poll();
    if (null != key) {
        map.remove(key);
        currentSize = map.size();
    }
}

public void put(Key key, Value val) {
    if (map.containsKey(key)) {// just heat up that item
        put(key, val);
        return;
    }
    while (currentSize >= maxSize) {
        freeSpace();
    }
    synchronized(this) {
        queue.add(key);
        map.put(key, val);
        currentSize++;
    }
}

public Value get(Key key) {
    return map.get(key);
}
}
于 2011-05-25T13:56:21.557 に答える
1

これがこの問題に対する私自身の実装です

simplelrucache は、TTL をサポートする、スレッドセーフで非常に単純な非分散型 LRU キャッシングを提供します。次の 2 つの実装を提供します。

  • ConcurrentLinkedHashMap に基づく同時実行
  • LinkedHashMap に基づいて同期

ここで見つけることができます: http://code.google.com/p/simplelrucache/

于 2011-12-21T12:18:24.463 に答える
1

ConcurrentSkipListMapを見てください。要素が既にキャッシュに含まれている場合は要素をテストして削除するための log(n) 時間と、要素を再度追加するための一定の時間を与える必要があります。

LRU順序の順序付けを強制し、キャッシュがいっぱいになったときに最近のものを確実に破棄するには、カウンターなどとラッパー要素が必要です。

于 2008-10-21T12:17:15.260 に答える
0

Java コードを使用した、より優れた LRU キャッシュを探しています。LinkedHashMapと を使用して Java LRU キャッシュ コードを共有することは可能Collections#synchronizedMapですか? 現在、私は使用してLRUMap implements Mapおり、コードは正常に動作ArrayIndexOutofBoundExceptionしますが、以下の方法で 500 ユーザーを使用して負荷テストを行っています。このメソッドは、最近のオブジェクトをキューの先頭に移動します。

private void moveToFront(int index) {
        if (listHead != index) {
            int thisNext = nextElement[index];
            int thisPrev = prevElement[index];
            nextElement[thisPrev] = thisNext;
            if (thisNext >= 0) {
                prevElement[thisNext] = thisPrev;
            } else {
                listTail = thisPrev;
            }
            //old listHead and new listHead say new is 1 and old was 0 then prev[1]= 1 is the head now so no previ so -1
            // prev[0 old head] = new head right ; next[new head] = old head
            prevElement[index] = -1;
            nextElement[index] = listHead;
            prevElement[listHead] = index;
            listHead = index;
        }
    }

get(Object key)put(Object key, Object value)メソッドは上記のメソッドを呼び出しますmoveToFront

于 2010-08-09T21:07:27.063 に答える
0

もう 1 つの考えと Java の LinkedHashMap コレクションを使用した簡単な実装です。

LinkedHashMap はメソッド removeEldestEntry を提供し、例で説明した方法でオーバーライドできます。デフォルトでは、このコレクション構造の実装は false です。true で、この構造体のサイズが初期容量を超えた場合、最も古い要素または古い要素が削除されます。

私の場合、pageno と page content を持つことができます pageno は整数で、pagecontent はページ番号の値の文字列を保持しています。

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author Deepak Singhvi
 *
 */
public class LRUCacheUsingLinkedHashMap {


     private static int CACHE_SIZE = 3;
     public static void main(String[] args) {
        System.out.println(" Pages for consideration : 2, 1, 0, 2, 8, 2, 4,99");
        System.out.println("----------------------------------------------\n");


// accessOrder is true, so whenever any page gets changed or accessed,    // its order will change in the map, 
              LinkedHashMap<Integer,String> lruCache = new              
                 LinkedHashMap<Integer,String>(CACHE_SIZE, .75F, true) {

           private static final long serialVersionUID = 1L;

           protected boolean removeEldestEntry(Map.Entry<Integer,String>                           

                     eldest) {
                          return size() > CACHE_SIZE;
                     }

                };

  lruCache.put(2, "2");
  lruCache.put(1, "1");
  lruCache.put(0, "0");
  System.out.println(lruCache + "  , After first 3 pages in cache");
  lruCache.put(2, "2");
  System.out.println(lruCache + "  , Page 2 became the latest page in the cache");
  lruCache.put(8, "8");
  System.out.println(lruCache + "  , Adding page 8, which removes eldest element 2 ");
  lruCache.put(2, "2");
  System.out.println(lruCache+ "  , Page 2 became the latest page in the cache");
  lruCache.put(4, "4");
  System.out.println(lruCache+ "  , Adding page 4, which removes eldest element 1 ");
  lruCache.put(99, "99");
  System.out.println(lruCache + " , Adding page 99, which removes eldest element 8 ");

     }

}

上記のコードの実行結果は次のとおりです。

 Pages for consideration : 2, 1, 0, 2, 8, 2, 4,99
--------------------------------------------------
    {2=2, 1=1, 0=0}  , After first 3 pages in cache
    {2=2, 1=1, 0=0}  , Page 2 became the latest page in the cache
    {1=1, 0=0, 8=8}  , Adding page 8, which removes eldest element 2 
    {0=0, 8=8, 2=2}  , Page 2 became the latest page in the cache
    {8=8, 2=2, 4=4}  , Adding page 4, which removes eldest element 1 
    {2=2, 4=4, 99=99} , Adding page 99, which removes eldest element 8 
于 2013-04-25T13:02:17.993 に答える
0

ハンクからの回答にコメントを追加したかったのですが、できないこともあります - コメントとして扱ってください

LinkedHashMap は、コンストラクターで渡されたパラメーターに基づいてアクセス順序も維持します。順序を維持するために二重に並んだリストを保持します (LinkedHashMap.Entry を参照)。

@Pacerier要素が再度追加された場合、LinkedHashMapが反復中に同じ順序を維持することは正しいですが、それは挿入順序モードの場合のみです。

これはLinkedHashMap.EntryオブジェクトのJavaドキュメントで見つけたものです

    /**
     * This method is invoked by the superclass whenever the value
     * of a pre-existing entry is read by Map.get or modified by Map.set.
     * If the enclosing Map is access-ordered, it moves the entry
     * to the end of the list; otherwise, it does nothing.
     */
    void recordAccess(HashMap<K,V> m) {
        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        if (lm.accessOrder) {
            lm.modCount++;
            remove();
            addBefore(lm.header);
        }
    }

このメソッドは、最近アクセスした要素をリストの最後に移動します。したがって、LinkedHashMap は、LRUCache を実装するための最適なデータ構造です。

于 2012-08-08T06:00:07.487 に答える
-1

Android はLRU キャッシュの実装を提供します。コードはクリーンで簡単です。

于 2014-09-18T09:07:11.417 に答える