JavaでLRUキャッシュをどのように実装しますか?


169

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

更新:このナゲットを見つけたとき、私はちょうどイェッジの最新を読んでいました:

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

LinkedHashMap+ と一緒に行く前に、私はほぼ同じことを考えていましたCollections#synchronizedMap上記の実装に。私が何かを見落としただけではなかったことを知ってうれしい。

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



回答:


102

私はこれらの提案の多くが好きですが、今のところ、私はLinkedHashMap+に固執すると思いますCollections.synchronizedMap。将来これを再検討する場合は、おそらくConcurrentHashMap同じ方法でLinkedHashMapextendsに取り組むことになりますHashMap

更新:

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

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));

15
ただし、継承ではなくカプセル化を使用したいと思います。これは、Effective Javaから学んだことです。
カピルD

10
@KapilD久しぶりですがLinkedHashMap、LRU実装を作成するためにこのメソッドを明示的に推奨しているJavaDocsはほぼ間違いありません。
ハンクゲイ

7
@HankGay JavaのLinkedHashMap(3番目のパラメーター= true)はLRUキャッシュではありません。これは、エントリの再
発行

2
@Pacerier私はこの振る舞いをまったく見ません。accessOrderが有効になっているマップでは、すべてのアクションが、最後に使用された(最新の)エントリを作成します:最初の挿入、値の更新、および値の取得。何か不足していますか?
エサイリヤ2013年

3
@Pacerier "エントリを再発行してもエントリの順序には影響しません"、これは正しくありません。LinkedHashMapの実装を見ると、「put」メソッドはHashMapから実装を継承しています。そして、HashMapのJavadocは、「マップが以前にキーのマッピングを含んでいた場合、古い値が置き換えられる」と述べています。そして、ソースコードをチェックアウトすると、古い値を置き換えるときに、recordAccessメソッドが呼び出され、LinkedHashMapのrecordAccessメソッドでは、次のようになります。if(lm.accessOrder){lm.modCount ++; 削除する(); addBefore(lm.header);}
nybon 2013


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サポートを追加したので、最初の試みはちょっとした災害でした...そして、それがモンスターになっていることに気付きました。次に、ほとんど関心のない仲間のJohnと話し始め、LFU、LRU、FIFOを実装する方法と、単純なENUM argでそれを切り替える方法を詳細に説明しました。単純な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を例に取り、要素からノードへの逆ルックアップハッシュマップを追加して、削除操作をより高速で一貫性のあるものにすることができます。私も始めましたが、それが必要ないことに気付きました。だけど、たぶん...

ときプットが呼ばれ、キーがキューに追加されます。得るとき呼ばれ、キーが削除されると、キューの先頭に再追加します。

キャッシュが小さく、アイテムの作成にコストがかかる場合、これは適切なキャッシュになるはずです。キャッシュが非常に大きい場合、特にキャッシュのホット領域がない場合、線形検索はボトルネックになる可能性があります。ホットスポットが激しいほど、リニアサーチの速度が上がります。これは、ホットアイテムが常にリニアサーチの先頭にあるためです。とにかく...これを高速化するために必要なのは、削除のためのノードルックアップに対する逆要素を持つ削除操作を持つ別の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を実装しようとしています。

    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には、ノード機能へのその逆の要素が必要になります。

とにかく...コードを書いたので、他の答えを調べて、何が欠けているかを見てみましょう...初めてスキャンしたとき。


9

LinkedHashMapO(1)ですが、同期が必要です。そこでホイールを再発明する必要はありません。

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

1.複数を作成しLinkedHashMap、それらにハッシュします:例:LinkedHashMap[4], index 0, 1, 2, 3。キーを押してkey%4 (またはbinary ORを押して[key, 3])、配置/取得/削除するマップを選択します。

2.を拡張しConcurrentHashMap、その内部の各領域にリンクされたハッシュマップのような構造を持たせることで、「ほぼ」LRUを実行できます。ロックLinkedHashMapは、同期されているものよりも細かく発生します。上putまたはputIfAbsentリストの先頭と末尾にのみロック(地域ごとに)必要とされています。削除または取得時に、領域全体をロックする必要があります。ある種のアトミックリンクリストがここで役立つかどうか、私は興味があります-おそらくリストのトップにとってはそうです。たぶんもっと。

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

  1. 中程度の並行性の下では、書き込みが容易で高速になります。

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


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/


2
Solrのソリューションは実際にはLRUではありませんが、ConcurrentLinkedHashMap興味深いものです。それはMapMakerグアバから巻き込まれたと主張していますが、私はドキュメントでそれを見つけませんでした。その努力で何が起こっているのでしょうか?
Hank Gay

3
簡略化されたバージョンが統合されましたが、テストが完了していないため、まだ公開されていません。より深い統合を行うには多くの問題がありましたが、いくつかの優れたアルゴリズムのプロパティがあるので、それを終えることを望みます。エビクション(容量、有効期限、GC)をリッスンする機能が追加され、CLHMのアプローチ(リスナーキュー)に基づいています。「加重値」のアイデアも寄稿したいと思います。これは、コレクションをキャッシュするときに役立ちます。残念ながら、他のコミットメントのために、グアバにふさわしい時間を費やすにはあまりにも多忙でした(そして私はケビン/チャールズを約束しました)。
Ben Manes

3
更新:統合が完了し、Guava r08で公開されました。これは#maximumSize()設定を使用して行います。
ベンマネス

7

各要素の "numberOfUses"カウンターによって優先順位が決定されるjava.util.concurrent.PriorityBlockingQueueの使用を検討します。私はとても、とても慎重になるでしょう「numberOfUses」カウンターは要素が不変ではないことすべての同期を正しく取得するためになりました。

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

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

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

    ... etc.
}

不変でなければならないという意味ですか?
shsteimer 2008年

2
steve mcleodによって言及されたpriorityblockingqueueバージョンを実行する場合は、要素を不変にする必要があることに注意してください。キュー内で要素を変更しても影響がないため、要素を削除して再度追加する必要があります。優先順位を付け直してください。
ジェームズ、

以下のジェームズは私が作ったエラーを指摘しています。信頼できる頑丈なキャッシュを書くのがどれほど難しいかについての証拠として私はそれを提供します。
スティーブマクロード

6

お役に立てれば 。

import java.util.*;
public class Lru {

public static <K,V> Map<K,V> lruCache(final int maxSize) {
    return new LinkedHashMap<K, V>(maxSize*4/3, 0.75f, true) {

        private static final long serialVersionUID = -3588047435434569014L;

        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > maxSize;
        }
    };
 }
 public static void main(String[] args ) {
    Map<Object, Object> lru = Lru.lruCache(2);      
    lru.put("1", "1");
    lru.put("2", "2");
    lru.put("3", "3");
    System.out.println(lru);
}
}

1
いい例です!容量maxSize * 4/3を設定する必要がある理由をコメントしていただけますか?
Akvel 2014年

1
@Akvelこれは初期容量と呼ばれ、任意の[整数]値にすることができますが、デフォルトの負荷係数は0.75fです。このリンクが役立つことを願っています:ashishsharma.me/2011/09/custom-lru-cache-java.html
murasing

5

LRUキャッシュは、マルチスレッドのシナリオでも使用できるConcurrentLinkedQueueとConcurrentHashMapを使用して実装できます。キューの先頭は、キューに最も長い間存在していた要素です。キューの末尾は、キューに最も短い時間存在していた要素です。要素がマップに存在する場合、LinkedQueueから要素を削除して、末尾に挿入できます。

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

public class LRUCache<K,V> {
  private ConcurrentHashMap<K,V> map;
  private ConcurrentLinkedQueue<K> queue;
  private final int size; 

  public LRUCache(int size) {
    this.size = size;
    map = new ConcurrentHashMap<K,V>(size);
    queue = new ConcurrentLinkedQueue<K>();
  }

  public V get(K key) {
    //Recently accessed, hence move it to the tail
    queue.remove(key);
    queue.add(key);
    return map.get(key);
  }

  public void put(K key, V value) {
    //ConcurrentHashMap doesn't allow null key or values
    if(key == null || value == null) throw new NullPointerException();
    if(map.containsKey(key) {
      queue.remove(key);
    }
    if(queue.size() >= size) {
      K lruKey = queue.poll();
      if(lruKey != null) {
        map.remove(lruKey);
      }
    }
    queue.add(key);
    map.put(key,value);
  }

}

これはスレッドセーフではありません。たとえば、を同時に呼び出すことにより、最大LRUサイズを簡単に超えることができますput
dpeacock 2015

修正してください。まず、それは行map.containsKey(key)でコンパイルされません。次に、get()でキーが本当に削除されたかどうかを確認する必要があります。そうしないと、マップとキューが同期しなくなり、「queue.size()> = size」が常にtrueになります。私はこれらの2つのコレクションを使用するというあなたのアイデアが好きだったので、これを修正したバージョンを投稿します。
Aleksander Lech

3

これがLRUの実装です。基本的にFIFOとして機能し、スレッドセーフではないPriorityQueueを使用しました。ページ時間の作成とに基づく中古コンパレータは、最も最近使用されていない時間のページの順序付けを実行します。

検討対象のページ:2、1、0、2、8、2、4

キャッシュに追加されたページは:2キャッシュに追加された
ページは:1
キャッシュに追加されたページは:0
ページ:2すでにキャッシュに存在します。最後にアクセスした時刻更新された
ページフォールト、ページ:1、ページで置き換え:8
ページがキャッシュに追加されました:8
ページ:2キャッシュに既に存在しています 最後にアクセスされた時刻更新
ページ違反、ページ:0、ページ:4で置換
キャッシュに追加されたページは:4

出力

LRUCacheページ
-------------
PageName:8、PageCreationTime:1365957019974
PageName:2、PageCreationTime:1365957020074
PageName:4、PageCreationTime:1365957020174

ここにコードを入力してください

import java.util.Comparator;
import java.util.Iterator;
import java.util.PriorityQueue;


public class LRUForCache {
    private PriorityQueue<LRUPage> priorityQueue = new PriorityQueue<LRUPage>(3, new LRUPageComparator());
    public static void main(String[] args) throws InterruptedException {

        System.out.println(" Pages for consideration : 2, 1, 0, 2, 8, 2, 4");
        System.out.println("----------------------------------------------\n");

        LRUForCache cache = new LRUForCache();
        cache.addPageToQueue(new LRUPage("2"));
        Thread.sleep(100);
        cache.addPageToQueue(new LRUPage("1"));
        Thread.sleep(100);
        cache.addPageToQueue(new LRUPage("0"));
        Thread.sleep(100);
        cache.addPageToQueue(new LRUPage("2"));
        Thread.sleep(100);
        cache.addPageToQueue(new LRUPage("8"));
        Thread.sleep(100);
        cache.addPageToQueue(new LRUPage("2"));
        Thread.sleep(100);
        cache.addPageToQueue(new LRUPage("4"));
        Thread.sleep(100);

        System.out.println("\nLRUCache Pages");
        System.out.println("-------------");
        cache.displayPriorityQueue();
    }


    public synchronized void  addPageToQueue(LRUPage page){
        boolean pageExists = false;
        if(priorityQueue.size() == 3){
            Iterator<LRUPage> iterator = priorityQueue.iterator();

            while(iterator.hasNext()){
                LRUPage next = iterator.next();
                if(next.getPageName().equals(page.getPageName())){
                    /* wanted to just change the time, so that no need to poll and add again.
                       but elements ordering does not happen, it happens only at the time of adding
                       to the queue

                       In case somebody finds it, plz let me know.
                     */
                    //next.setPageCreationTime(page.getPageCreationTime()); 

                    priorityQueue.remove(next);
                    System.out.println("Page: " + page.getPageName() + " already exisit in cache. Last accessed time updated");
                    pageExists = true;
                    break;
                }
            }
            if(!pageExists){
                // enable it for printing the queue elemnts
                //System.out.println(priorityQueue);
                LRUPage poll = priorityQueue.poll();
                System.out.println("Page Fault, PAGE: " + poll.getPageName()+", Replaced with PAGE: "+page.getPageName());

            }
        }
        if(!pageExists){
            System.out.println("Page added into cache is : " + page.getPageName());
        }
        priorityQueue.add(page);

    }

    public void displayPriorityQueue(){
        Iterator<LRUPage> iterator = priorityQueue.iterator();
        while(iterator.hasNext()){
            LRUPage next = iterator.next();
            System.out.println(next);
        }
    }
}

class LRUPage{
    private String pageName;
    private long pageCreationTime;
    public LRUPage(String pagename){
        this.pageName = pagename;
        this.pageCreationTime = System.currentTimeMillis();
    }

    public String getPageName() {
        return pageName;
    }

    public long getPageCreationTime() {
        return pageCreationTime;
    }

    public void setPageCreationTime(long pageCreationTime) {
        this.pageCreationTime = pageCreationTime;
    }

    @Override
    public boolean equals(Object obj) {
        LRUPage page = (LRUPage)obj; 
        if(pageCreationTime == page.pageCreationTime){
            return true;
        }
        return false;
    }

    @Override
    public int hashCode() {
        return (int) (31 * pageCreationTime);
    }

    @Override
    public String toString() {
        return "PageName: " + pageName +", PageCreationTime: "+pageCreationTime;
    }
}


class LRUPageComparator implements Comparator<LRUPage>{

    @Override
    public int compare(LRUPage o1, LRUPage o2) {
        if(o1.getPageCreationTime() > o2.getPageCreationTime()){
            return 1;
        }
        if(o1.getPageCreationTime() < o2.getPageCreationTime()){
            return -1;
        }
        return 0;
    }
}

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);
}

}


1
@zoltan boda .... 1つの状況を処理していません..同じオブジェクトが複数回使用された場合はどうなりますか?この場合、同じオブジェクトに複数のエントリを追加するべきではありません...代わりに、そのキーは次のようになります

5
警告:これはLRUキャッシュではありません。LRUキャッシュでは、最も最近アクセスされたアイテムを破棄します。これは最も最近に書かれたアイテムを捨てます。また、queue.remove(key)操作を実行するための線形スキャンでもあります。
Dave L.

また、ConcurrentLinkedQueue#size()は一定時間の操作ではありません。
NateS

3
putメソッドは安全に見えません-複数のスレッドで中断するいくつかのcheck-then-actステートメントがあります。
アッシリアス

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();
        }
    }

}

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変数を使用して実装できる追加の最適化がいくつかあると確信しています。


詳細のレベルに感謝しますが、これはLinkedHashMap+ Collections.synchronizedMap()実装よりも優れているのでしょうか?
ハンクゲイ

パフォーマンス、確かなことはわかりませんが、LinkedHashMapにO(1)挿入(おそらくO(log(n)))があるとは思いません。実際に、実装にマップインターフェイスを完成させるためにいくつかのメソッドを追加できます次に、Collections.synchronizedMapを使用して同時実行性を追加します。
luke

上のLinkedListクラスのaddメソッドには、elseブロックにコードがあります。つまり、if(tail.prev == null){tail.prev = head; head.next = tail; このコードはいつ実行されますか?私はいくつかの予行演習を実行しましたが、これは決して実行されず、削除する必要があると思います。
ディペッシュ

1

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

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


ConcurrentSkipListMap比べて実装が容易になるというメリットがConcurrentHashMapありますか、それとも単に病理的なケースを回避するためのケースですか?
ハンクゲイ

ConcurrentSkipListMapが要素を順序付けするので、物事がより簡単になります。これにより、物事が使用された順序を管理できます。ConcurrentHashMapはこれを行わないため、基本的にキャッシュコンテンツ全体を反復して、要素の「最後」を更新する必要があります。使用済みカウンター」など
madlep

したがって、ConcurrentSkipListMap実装では、任意のキータイプが最後のアクセスに基づいて簡単にソートできるタイプにラップされるように、MapデリゲートしConcurrentSkipListMapて何らかのラッピングを実行するインターフェイスの新しい実装を作成しますか?
ハンクゲイ

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);
}
}

1
これはLRUキャッシュではなく、FIFOキャッシュです。
lslab 2013年

1

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

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

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

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


1

達成する最良の方法は、要素の挿入順序を維持するLinkedHashMapを使用することです。以下はサンプルコードです:

public class Solution {

Map<Integer,Integer> cache;
int capacity;
public Solution(int capacity) {
    this.cache = new LinkedHashMap<Integer,Integer>(capacity); 
    this.capacity = capacity;

}

// This function returns false if key is not 
// present in cache. Else it moves the key to 
// front by first removing it and then adding 
// it, and returns true. 

public int get(int key) {
if (!cache.containsKey(key)) 
        return -1; 
    int value = cache.get(key);
    cache.remove(key); 
    cache.put(key,value); 
    return cache.get(key); 

}

public void set(int key, int value) {

    // If already present, then  
    // remove it first we are going to add later 
       if(cache.containsKey(key)){
        cache.remove(key);
    }
     // If cache size is full, remove the least 
    // recently used. 
    else if (cache.size() == capacity) { 
        Iterator<Integer> iterator = cache.keySet().iterator();
        cache.remove(iterator.next()); 
    }
        cache.put(key,value);
}

}


0

Javaコードを使用してより良いLRUキャッシュを探しています。LinkedHashMapand を使用して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


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を実装するのに最適なデータ構造です。


0

別の考え、さらにはJavaのLinkedHashMapコレクションを使用した単純な実装です。

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

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

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 

それはFIFOです。彼はLRUを要求しました。
RickHigh

このテストは失敗します... cache.get(2); cache.get(3); cache.put(6、6); cache.put(7、7); わかりました| = cache.size()== 4 || 死ぬ( "サイズ" + cache.size()); ok | = cache.getSilent(2)== 2 || 死ぬ(); ok | = cache.getSilent(3)== 3 || 死ぬ(); ok | = cache.getSilent(4)== null || 死ぬ(); ok | = cache.getSilent(5)== null || 死ぬ();
RickHigh

0

@sanjanabコンセプトに従って(ただし修正後)、必要に応じて削除されたアイテムで何かを実行できるコンシューマも提供するLRUCacheのバージョンを作成しました。

public class LRUCache<K, V> {

    private ConcurrentHashMap<K, V> map;
    private final Consumer<V> onRemove;
    private ConcurrentLinkedQueue<K> queue;
    private final int size;

    public LRUCache(int size, Consumer<V> onRemove) {
        this.size = size;
        this.onRemove = onRemove;
        this.map = new ConcurrentHashMap<>(size);
        this.queue = new ConcurrentLinkedQueue<>();
    }

    public V get(K key) {
        //Recently accessed, hence move it to the tail
        if (queue.remove(key)) {
            queue.add(key);
            return map.get(key);
        }
        return null;
    }

    public void put(K key, V value) {
        //ConcurrentHashMap doesn't allow null key or values
        if (key == null || value == null) throw new IllegalArgumentException("key and value cannot be null!");

        V existing = map.get(key);
        if (existing != null) {
            queue.remove(key);
            onRemove.accept(existing);
        }

        if (map.size() >= size) {
            K lruKey = queue.poll();
            if (lruKey != null) {
                V removed = map.remove(lruKey);
                onRemove.accept(removed);
            }
        }
        queue.add(key);
        map.put(key, value);
    }
}

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.