Java'da LRU önbelleğini nasıl uygularsınız?


169

Lütfen EHCache veya OSCache, vb. Demeyin. Bu sorunun amaçları doğrultusunda, sadece SDK kullanarak kendimi uygulamak istediğimi varsayalım (yaparak öğrenerek). Önbelleğin çok iş parçacıklı bir ortamda kullanılacağı göz önüne alındığında, hangi veri yapılarını kullanırsınız? LinkedHashMap ve Collections # senkronizeMap kullanarak bir tane uyguladım , ancak yeni eşzamanlı koleksiyonlardan herhangi birinin daha iyi aday olup olmayacağını merak ediyorum.

GÜNCELLEME: Bu külçeyi bulduğumda Yegge'nin son haberlerini okuyordum :

Sabit zamanlı erişime ihtiyacınız varsa ve ekleme siparişini korumak istiyorsanız, gerçekten harika bir veri yapısı olan LinkedHashMap'ten daha iyisini yapamazsınız. Muhtemelen daha harika olabilmesinin tek yolu, eşzamanlı bir versiyonun olup olmadığıdır. Ama ne yazık ki.

Yukarıda bahsettiğim LinkedHashMap+ Collections#synchronizedMapuygulamasına geçmeden önce neredeyse aynı şeyi düşünüyordum . Sadece bir şeyi göz ardı etmediğimi bilmek güzel.

Şimdiye kadar verilen cevaplara dayanarak, son derece eşzamanlı bir LRU için en iyi bahse benzeyen , aynı mantıktan bazılarını kullanarak ConcurrentHashMap'i genişletmek olurdu LinkedHashMap.



Yanıtlar:


102

Bu önerilerin çoğunu seviyorum, ancak şimdilik LinkedHashMap+ ile devam edeceğimi düşünüyorum Collections.synchronizedMap. Gelecekte tekrar ziyaret edersem, muhtemelen ConcurrentHashMapaynı şekilde LinkedHashMapuzanmaya çalışacağım HashMap.

GÜNCELLEME:

İstek üzerine, şu anki uygulamamın özeti burada.

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
Ancak burada kalıtım yerine kapsülleme kullanmak istiyorum. Bu Etkili Java'dan öğrendiğim bir şey.
Kapil D

10
@KapilD Bir süre oldu, ama LinkedHashMapbir LRU uygulaması oluşturmak için bu yöntemi açıkça onayladığı için JavaDocs neredeyse olumluyum .
Hank Gay

7
@HankGay Java'nın LinkedHashMap'i (üçüncü parametre = true ile) bir LRU önbelleği değildir. Bunun nedeni, bir girdinin yeniden yerleştirilmesinin girdilerin sırasını etkilememesidir (gerçek bir LRU önbelleği, bu girdinin başlangıçta önbellekte olup olmadığına bakılmaksızın son eklenen girdiyi yineleme sırasının arkasına yerleştirir)
Pacerier

2
@Pacerier Bu davranışı hiç görmüyorum. AccessOrder etkinleştirilmiş harita ile, tüm eylemler en son kullanılan (en taze) giriş yapar: ilk ekleme, değer güncelleme ve değer alımı. Bir şey mi kaçırıyorum?
Esailija

3
@Pacerier "bir girdiyi yeniden koymak girdilerin sırasını etkilemez", bu yanlıştır. LinkedHashMap uygulamasına bakarsanız, "put" yöntemi için, uygulamayı HashMap'ten devralır. HashMap'ten Javadoc "Harita daha önce anahtar için bir eşleme içeriyorsa, eski değer değiştirilir" diyor. Kaynak kodunu kontrol ederseniz, eski değeri değiştirirken, recordAccess yöntemini çağırır ve LinkedHashMap'in recordAccess yönteminde şöyle görünür: if (lm.accessOrder) {lm.modCount ++; Kaldırmak(); addBefore (lm.header);}
nybon


10

Bu ikinci tur.

İlk tur, ortaya çıkardığım şeydi, sonra yorumları kafamda biraz daha kökleşmiş olan alanlarla tekrar okudum.

İşte burada, bazı diğer sürümlere göre çalıştığını gösteren bir birim testi ile en basit sürüm.

İlk olarak eşzamanlı olmayan sürüm:

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


}

Gerçek bayrak, alma ve koyma erişimini izler. Bkz. JavaDocs. Yapıcıya gerçek bayrağı olmadan removeEdelstEntry yalnızca bir FIFO önbelleği uygular (aşağıdaki FIFO ve removeEldestEntry ile ilgili notlara bakın).

İşte LRU önbellek olarak çalıştığını kanıtlayan test:

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

    }

Şimdi eşzamanlı sürüm için ...

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


}

Öncelikle eşzamanlı olmayan sürümü neden kapsadığımı görebilirsiniz. Yukarıda kilit çekişmeyi azaltmak için bazı şeritler oluşturmaya çalışır. Bu yüzden anahtarı hash eder ve sonra gerçek önbelleği bulmak için bu hash arar. Bu, anahtar boyutunu karma algoritmasının ne kadar iyi yayıldığına bağlı olarak, sınır boyutunu makul miktarda hata içinde bir öneri / kaba tahminden daha fazla yapar.

Eşzamanlı sürümün muhtemelen çalıştığını gösteren test. :) (Ateş altında test gerçek yol olurdu).

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



    }

}

Bu son gönderi .. LRU önbellek değil LFU olduğu için sildiğim ilk gönderi.

Bunu bir kez daha vereceğimi düşündüm. Standart JDK çok fazla uygulama kullanarak bir LRU önbelleğinin en basit sürümünü bulmaya çalışıyordum.

İşte ortaya çıkardığım şey. İlk denemem bir LFU yerine ve LRU yerine biraz felaket oldu ve sonra FIFO ve LRU desteği ekledim ... ve sonra bir canavar haline geldiğini fark ettim. Sonra zorlukla ilgilenen arkadaşım John'la konuşmaya başladım ve daha sonra LFU, LRU ve FIFO'yu nasıl uyguladığımı ve basit bir ENUM argümanıyla nasıl değiştirebileceğinizi açıkladım ve sonra gerçekten istediğimi fark ettim basit bir LRU idi. Bu yüzden benden önceki gönderiyi görmezden gelin ve bir numaralandırma yoluyla değiştirilebilir bir LRU / LFU / FIFO önbelleği görmek istiyorsanız bana bildirin ... hayır? Tamam .. işte gidiyor.

Sadece JDK kullanarak mümkün olan en basit LRU. Hem eşzamanlı hem de eşzamanlı olmayan bir sürüm uyguladım.

Ortak bir arayüz oluşturdum (minimalizm çok büyük bir olasılıkla istediğiniz birkaç özelliği eksik ama kullanım durumlarım için çalışıyor, ancak XYZ özelliğini görmek isterseniz izin verin bana bildirin ... Kod yazmak için yaşıyorum.) .

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'in ne olduğunu merak edebilirsiniz . Bunu test için kullanıyorum. getSilent bir öğenin LRU puanını değiştirmez.

Önce eşzamanlı olmayan ....

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 Büyük bir önbellek varsa potansiyel olarak pahalı bir işlemdir. Bir örnek olarak LinkedList alabilir ve kaldırma işlemleri çok daha hızlı ve daha tutarlı hale getirmek için öğeden düğüme bir ters arama karma haritası eklenebilir. Ben de başladım ama sonra ihtiyacım olmadığını fark ettim. Ama belki...

Ne zaman koymak denir, anahtar kuyruğuna eklenir. Ne zaman olsun denir, anahtar kaldırılırsa ve sıranın üstüne yeniden eklendi.

Önbelleğiniz küçükse ve bir öğe oluşturmak pahalıysa, bu iyi bir önbellek olmalıdır. Önbelleğiniz gerçekten büyükse, özellikle sıcak önbellek alanlarınız yoksa doğrusal arama bir şişe boynu olabilir. Sıcak noktalar ne kadar yoğun olursa, sıcak öğeler her zaman doğrusal aramanın en üstünde olduğu için doğrusal arama o kadar hızlı olur. Her neyse ... bunun daha hızlı gitmesi için gereken şey, kaldırmak için düğüm aramasına ters eleman içeren bir kaldırma işlemi olan başka bir LinkedList yazmaktır, daha sonra kaldırma, bir karma haritasından bir anahtarı kaldırmak kadar hızlı olur.

1.000 öğenin altında bir önbelleğe sahipseniz, bunun iyi olması gerekir.

İşte operasyonlarını eylem halinde göstermek için basit bir test.

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

    }
}

Son LRU önbellek tek iş parçacıklı idi ve lütfen senkronize bir şey sarmayın ....

Eşzamanlı versiyonda bir bıçak.

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

Temel farklılıklar HashMap yerine ConcurrentHashMap kullanımı ve Lock kullanımıdır (senkronize edilmiş olabilirim, ama ...).

Ateş altında test etmedim, ancak basit bir LRU haritasına ihtiyacınız olan kullanım durumlarının% 80'inde çalışabilecek basit bir LRU önbelleği gibi görünüyor.

Neden a, b veya c kitaplığını kullanmamanız dışında geri bildirim almaktan memnuniyet duyuyorum. Her zaman bir kütüphane kullanmamamın nedeni, her savaş dosyasının her zaman 80MB olmasını istememem ve kütüphaneler yazmamdan dolayı kütüphaneleri yerinde yeterince iyi bir çözümle takılabilir hale getirme eğilimindeyim ve birisi takabilir -eğer isterse başka bir önbellek sağlayıcısında. :) Birisinin ne zaman Guava veya ehcache'ye veya başka bir şeye ihtiyaç duyduğunu asla bilemeyeceğim, ancak bunları dahil etmek istemiyorum, ancak önbelleğe alma eklentisini yaparsam, onları da hariç tutmayacağım.

Bağımlılıkların azaltılmasının kendi ödülü vardır. Bunu nasıl daha basit, daha hızlı veya her ikisini birden yapabileceğiniz hakkında geri bildirim almayı seviyorum.

Ayrıca kimse gitmek için hazır bir biliyorsa ....

Tamam .. Ne düşündüğünü biliyorum ... Neden sadece LinkedHashMap'ten removeEldest girdisini kullanmıyor ve ben yapmalıyım ama .... ama .. ama .. Bu bir LRU değil bir FIFO olurdu ve biz LRU uygulamaya çalışıyor.

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

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

Bu test yukarıdaki kod için başarısız oluyor ...

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

Burada removeEldestEntry kullanarak hızlı ve kirli bir FIFO önbelleği var.

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'lar hızlı. Etrafta arama yok. Bir LRU önünde bir FIFO ön olabilir ve bu çoğu sıcak girişleri oldukça güzel idare edecek. Daha iyi bir LRU, bu ters elemanı Düğüm özelliğine ihtiyaç duyacaktır.

Her neyse ... şimdi bir kod yazdığım için, diğer cevapları gözden geçirmeme ve neyi kaçırdığımı görmeme izin verin ... onları ilk taradığımda.


9

LinkedHashMapO (1) 'dir, ancak senkronizasyon gerektirir. Orada tekerleği yeniden keşfetmeye gerek yok.

Eşzamanlılığı artırmak için 2 seçenek:

Birden çok oluşturun 1. LinkedHashMapiçlerine ve karma: example: LinkedHashMap[4], index 0, 1, 2, 3. Anahtarda , koymak / almak / kaldırmak için hangi haritayı seçmek için key%4 (veya binary ORaçık [key, 3]) yapın.

2. ConcurrentHashMapİçindeki bölgelerin her birinde genişleyerek ve bağlantılı bir karma harita benzeri yapıya sahip olarak 'neredeyse' bir LRU yapabilirsiniz . Kilitleme LinkedHashMap, senkronize edilmiş olandan daha ayrıntılı olarak gerçekleşir . Listenin başına ve kuyruğuna bir putveya putIfAbsentsadece bir kilit gerekir (bölge başına). Bir kaldırımda veya tüm bölgenin kilitli olması gerekir. Bir tür Atomik bağlantılı listenin burada yardımcı olup olmayacağını merak ediyorum - muhtemelen listenin başı için. Belki daha fazlası için.

Yapı toplam siparişi değil, sadece bölge başına siparişi koruyacaktır. Giriş sayısı bölge sayısından çok daha fazla olduğu sürece, bu çoğu önbellek için yeterince iyidir. Her bölgenin kendi giriş sayısı olmalıdır, bu tahliye tetikleyicisi için küresel sayım yerine kullanılacaktır. A'daki varsayılan bölge sayısı, ConcurrentHashMapbugün çoğu sunucu için bol olan 16'dır.

  1. ılımlı eşzamanlılık altında yazmak daha kolay ve daha hızlı olurdu.

  2. yazmak daha zordur, ancak çok yüksek eşzamanlılıkta çok daha iyi ölçeklenir. Normal erişim için daha yavaş olacaktır ( aynı eşzamanlılığın ConcurrentHashMapolmadığı HashMapyerden daha yavaş olduğu gibi )


8

İki açık kaynak uygulaması vardır.

Apache Solr EşzamanlıLRUCache'ye sahiptir: https://lucene.apache.org/solr/3_6_1/org/apache/solr/util/ConcurrentLRUCache.html

ConcurrentLinkedHashMap için açık kaynaklı bir proje var: http://code.google.com/p/concurrentlinkedhashmap/


2
Solr'un çözümü aslında LRU değil ConcurrentLinkedHashMap, ilginç. MapMakerGuava'dan alındığını iddia ediyor , ancak dokümanlarda tespit etmedim. Bu çabaya ne olduğuna dair bir fikrin var mı?
Hank Gay

3
Basitleştirilmiş bir sürüm entegre edildi, ancak testler henüz tamamlanmadığı için henüz tamamlanmadı. Daha derin bir entegrasyon yaparken birçok sorunum vardı, ancak bazı güzel algoritmik özellikler olduğu için bitirmeyi umuyorum. Bir tahliyeyi dinleme kapasitesi (kapasite, son kullanma tarihi, GC) eklenmiştir ve CLHM yaklaşımına (dinleyici kuyruğu) dayanmaktadır. Ayrıca "ağırlıklı değerler" fikrine de katkıda bulunmak istiyorum, çünkü koleksiyonları önbelleğe alırken faydalıdır. Ne yazık ki diğer taahhütler nedeniyle Guava'nın hak ettiği zamanı (ve Kevin / Charles'a söz verdiğimi) ayırmak için fazla gömüldüm.
Ben Manes

3
Güncelleme: Entegrasyon tamamlandı ve Guava r08'de herkese açık. Bu işlem #maximumSize () ayarı üzerinden yapılır.
Ben Manes

7

Öncelik her öğede bir "numberOfUses" sayacı tarafından belirlenen java.util.concurrent.PriorityBlockingQueue kullanmayı düşünün . Ben olacağını çok ama çok dikkatli "numberOfUses" sayaç elemanı değişmez olamayacağını da anlaşılacağı gibi, bütün senkronizasyon doğru olsun.

Öğe nesnesi, önbellekteki nesneler için bir sarıcı olacaktır:

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

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

    ... etc.
}

yani değişmez olmalı mı demek istiyorsun?
shsteimer

2
steve mcleod tarafından belirtilen Priorityblockingqueue sürümünü yapmaya çalışırsanız, öğeyi değiştirilemez hale getirmelisiniz, çünkü kuyruktayken öğeyi değiştirmenin bir etkisi olmayacaktır, öğeyi kaldırmanız ve yeniden eklemek için yeniden önceliklendirin.
james

James aşağıda yaptığım bir hatayı gösteriyor. Hangi güvenilir, sağlam önbellek yazmak ne kadar zor kanama olduğunu kanıt olarak sunuyoruz.
Steve McLeod

6

Bu yardımcı olur umarım .

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
Güzel örnek! Neden maxSize * 4/3 kapasitesini ayarlamanız gerektiğini yorumlayabilir misiniz?
Akvel

1
: Herhangi bu bağlantıyı yardımcı olur umarım [tamsayı] 0.75f oysa değeri, varsayılan yük faktörüdür olabilir, bu başlangıç kapasitesi denir @Akvel ashishsharma.me/2011/09/custom-lru-cache-java.html
murasing

5

LRU Önbellek bir ConcurrentLinkedQueue ve çoklu iş parçacığı senaryosunda da kullanılabilen bir ConcurrentHashMap kullanılarak uygulanabilir. Kuyruğun başı, kuyrukta en uzun süredir var olan öğedir. Kuyruğun kuyruğu, kuyrukta en kısa süredir var olan elementtir. Haritada bir öğe bulunduğunda, onu LinkedQueue'dan kaldırabilir ve kuyruğa ekleyebiliriz.

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

}

Bu güvenli değil . Örneğin, aynı anda arayarak maksimum LRU boyutunu kolayca aşabilirsiniz put.
dpeacock

Lütfen düzeltin. Her şeyden önce çizgi haritası üzerinde derlenmez. ContainsKey (anahtar). İkincisi get () 'de anahtarın gerçekten kaldırılıp kaldırılmadığını kontrol etmelisiniz, aksi takdirde harita ve kuyruk senkronize olmaz ve "queue.size ()> = size" her zaman doğru olur. Bu iki koleksiyonu kullanma fikrinizi sevdiğim için bu düzeltmeyi içeren sürümümü yayınlayacağım.
Aleksander Lech

3

İşte benim LRU için benim uygulama. Ben temelde FIFO olarak çalışır ve threadsafe değil PriorityQueue kullandık. Kullanılan Karşılaştırıcı, sayfa süresi oluşturulmasına ve sayfaların en son kullanılan süre için sırasına göre yapılır.

Dikkate alınacak sayfalar: 2, 1, 0, 2, 8, 2, 4

Önbelleğe eklenen sayfa: 2
Önbelleğe eklenen sayfa: 1
Önbelleğe eklenen sayfa: 0
Sayfa: 2 zaten önbellekte mevcut. En son erişilen süre güncellendi
Sayfa Hatası, SAYFA: 1, SAYFA ile değiştirildi: 8
Önbelleğe eklenen sayfa: 8
Sayfa: 2 zaten önbellekte mevcut. En son erişilen süre güncellendi
Sayfa Hatası, PAGE: 0, PAGE ile değiştirildi: 4
Önbelleğe eklenen sayfa: 4

ÇIKTI

LRUCache Sayfaları
------------- Sayfa
Adı: 8, Sayfa Oluşturma Zamanı: 1365957019974 Sayfa
Adı: 2, Sayfa Oluşturma Zamanı: 1365957020074 Sayfa
Adı: 4, Sayfa Oluşturma Zamanı: 1365957020174

kodu buraya girin

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

İşte herhangi bir senkronize blok olmadan test benim en iyi performans eşzamanlı LRU önbellek uygulaması:

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 .... bir durumu ele almadınız .. ya aynı nesne birden çok kez kullanılırsa? bu durumda aynı nesne için birden fazla giriş eklememeliyiz ... bunun yerine anahtarı şu şekilde olmalıdır

5
Uyarı: Bu bir LRU önbelleği değildir. LRU önbelleğinde, en son erişilen öğeleri atarsınız. Bu, en son yazılan öğeleri atar. Ayrıca queue.remove (anahtar) işlemini gerçekleştirmek için doğrusal bir taramadır.
Dave L.

Ayrıca ConcurrentLinkedQueue # size () sabit bir zaman işlemi değildir.
NateS

3
Put yönteminiz güvenli görünmüyor - birden fazla iş parçacığıyla kesilecek birkaç check-then-act ifadesi var.
assylias

2

Bu, LinkedHashMap'i içine alan ve sulu noktaları koruyan basit bir senkronizasyon kilidiyle eşzamanlılığı işleyen LRU önbelleğidir. Kullanılan elemanlara tekrar "en taze" eleman olmaları için dokunur, böylece aslında LRU'dur. Ayrıca, elementlerimin minimum kullanım ömrüne sahip olması şartı da vardı, bu da izin verilen "maksimum boşta kalma süresi" olarak düşünülebilir, o zaman tahliye edersiniz.

Bununla birlikte, Hank'in sonucuna katılıyorum ve cevabı kabul ettim - bugün tekrar başlasaydım, Guava'nın kontrolüne giderdim 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

Bir önbellek için genellikle bir proxy nesnesi (URL, String ....) aracılığıyla bir parça veri arayacaksınız, böylece arayüz açısından bir harita isteyeceksiniz. ama bir şeyleri dışarı atmak için sıra benzeri bir yapı istiyorsunuz. Dahili olarak bir veri önceliği-sırası ve bir HashMap olmak üzere iki veri yapısı korumak istiyorum. O (1) zamanında her şeyi yapabilmesi gereken bir uygulama vardır.

İşte oldukça hızlı bir şekilde çırptığım bir sınıf:

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

İşte böyle. Anahtarlar, listenin önündeki en eski anahtarlar (yeni anahtarlar arkaya gider) ile bağlantılı bir listede saklanır, böylece bir şeyi 'çıkarmanız' gerektiğinde kuyruğun önünden çıkarırsınız ve ardından değeri haritadan kaldırın. Bir öğeye başvurulduğunda, ValueHolder'ı haritadan alırsınız ve daha sonra anahtarı kuyruktaki geçerli konumundan kaldırmak ve ardından kuyruğun arkasına (şimdi en son kullanılan) koymak için kuyruk konumu değişkenini kullanırsınız. Bir şeyler eklemek hemen hemen aynı.

Eminim burada bir ton hata var ve herhangi bir senkronizasyon uygulamadım. ancak bu sınıf önbelleğe O (1) ekleme, eski öğelerin O (1) kaldırılmasını ve önbellek öğelerinin O (1) alınmasını sağlayacaktır. Önemsiz bir senkronizasyon bile (sadece herkese açık her yöntemi senkronize edin) çalışma süresi nedeniyle hala çok az kilit tartışmasına sahip olacaktır. Herhangi bir akıllı senkronizasyon hilesi varsa, ben çok ilgi olurdu. Ayrıca, haritaya göre maxsize değişkenini kullanarak uygulayabileceğiniz bazı ek optimizasyonlar olduğundan eminim.


Ayrıntı düzeyi için teşekkürler, ancak bu LinkedHashMap+ Collections.synchronizedMap()uygulama üzerinde nasıl bir kazanç sağlar ?
Hank Gay

Performans, emin değilim, ama LinkedHashMap O (1) ekleme (muhtemelen O (log (n))) olduğunu düşünmüyorum, aslında benim uygulamada harita arabirimini tamamlamak için birkaç yöntem ekleyebilirsiniz ve ardından eşzamanlılık eklemek için Collections.synchronizedMap öğesini kullanın.
luke

Yukarıdaki addL yönteminde LinkedList sınıfında, başka bir blokta bir kod vardır, yani if ​​(tail.prev == null) {tail.prev = head; head.next = kuyruk; } Bu kod ne zaman yürütülecek? Birkaç kuru koşu koştum ve sanırım bu asla idam edilmeyecek ve kaldırılmalı.
Dipesh

1

ConcurrentSkipListMap'e bir göz atın . Önceden önbellekte bulunuyorsa, size bir öğeyi test etmek ve kaldırmak için günlük (n) süresi ve yeniden eklemek için sabit bir süre vermelidir.

LRU siparişinin sırasını zorlamak ve önbellek dolduğunda son öğelerin atılmasını sağlamak için bazı sayaç vb.


ConcurrentSkipListMapUygulama kolaylığı açısından fayda sağlayabilir mi ConcurrentHashMapyoksa sadece patolojik durumlardan kaçınma vakası mıdır?
Hank Gay

ConcurrentSkipListMap sipariş öğeleri olarak, şeylerin hangi sırada kullanıldığını yönetmenize izin verecek şekilde işleri kolaylaştırır. ConcurrentHashMap bunu yapmaz, bu nedenle bir öğenin son öğesini güncellemek için temel olarak tüm önbellek içeriğini yinelemeniz gerekir kullanılan sayaç 'ya da ne olursa olsun
madlep

Böylece ConcurrentSkipListMapuygulama ile, keyfi anahtar türleri son erişime göre kolayca sıralanan bir tür sarılır böylece bir tür sarma Mapdelege ConcurrentSkipListMapve gerçekleştiren arabirim yeni bir uygulama oluşturmak ?
Hank Gay

1

İşte benim kısa uygulama, lütfen eleştirmek veya geliştirmek!

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
Bu LRU önbellek değil, sadece FIFO önbellek.
lslab

1

İşte bu soruna kendi uygulamam

simplelrucache, TTL desteği ile güvenli, çok basit, dağıtılmamış LRU önbellekleme sağlar. İki uygulama sağlar:

  • Eşzamanlı EşzamanlıBağlantılıHashMap
  • LinkedHashMap temel alınarak senkronize edildi

Burada bulabilirsiniz: http://code.google.com/p/simplelrucache/


1

Bunu başarmanın en iyi yolu, öğelerin ekleme sırasını koruyan LinkedHashMap kullanmaktır. Aşağıda bir örnek kod verilmiştir:

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 kodunu kullanarak daha iyi bir LRU önbellek arıyorum. Java LRU önbellek kodunuzu LinkedHashMapve tuşlarını kullanarak paylaşmanız mümkün müdür Collections#synchronizedMap? Şu anda kullanıyorum LRUMap implements Mapve kod iyi çalışıyor, ancak ArrayIndexOutofBoundExceptionaşağıdaki yöntemde 500 kullanıcı kullanarak yük testi alıyorum . Yöntem, son nesneyi sıranın önüne taşır.

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)ve put(Object key, Object value)yöntem yukarıdaki moveToFrontyöntemi çağırır .


0

Hank tarafından verilen cevaba yorum eklemek istedim, ancak bazılarını nasıl yapamıyorum - lütfen yorum olarak ele alın

LinkedHashMap, yapıcısında geçirilen parametreye bağlı olarak erişim sırasını da korur. Siparişi korumak için iki kez sıralı liste tutar (Bkz. LinkedHashMap.Entry)

@Pacerier öğesi tekrar eklenirse, LinkedHashMap öğesinin yineleme sırasında aynı sırada kalması doğrudur, ancak bu yalnızca ekleme siparişi modundadır.

LinkedHashMap.Entry nesnesinin java belgelerinde bulduğum şey bu

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

bu yöntem, son erişilen öğenin listenin sonuna taşınmasını sağlar. Sonuçta LinkedHashMap, LRUCache uygulamak için en iyi veri yapısıdır.


0

Bir başka düşünce ve hatta LinkedHashMap Java koleksiyonu kullanarak basit bir uygulama.

LinkedHashMap, removeEldestEntry yöntemini sağladı ve örnekte belirtilen şekilde geçersiz kılınabilir. Varsayılan olarak bu toplama yapısının uygulanması yanlıştır. Eğer bu yapının gerçek ve boyutu başlangıç ​​kapasitesinin ötesine geçerse en büyük veya daha eski elemanlar kaldırılır.

Benim durumumda bir pageno ve sayfa içeriği olabilir pageno tam sayı ve pagecontent i sayfa numarası değerleri dizesini tuttu.

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

     }

}

Yukarıdaki kod yürütme sonucu aşağıdaki gibidir:

 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 

Bu bir FIFO. LRU istedi.
RickHigh

Bu testi geçemez ... cache.get (2); cache.get (3); önbellek girdisi (6, 6); önbellek girdisi (7, 7); ok | = cache.size () == 4 || die ("boyut" + cache.size ()); ok | = cache.getSilent (2) == 2 || ölmek (); ok | = cache.getSilent (3) == 3 || ölmek (); ok | = cache.getSilent (4) == null || ölmek (); ok | = cache.getSilent (5) == null || ölmek ();
RickHigh

0

@Sanjanab konseptini (ancak düzeltmelerden sonra) takiben, LRUCache versiyonumu da, gerekirse kaldırılan öğelerle bir şeyler yapmayı sağlayan Tüketici'yi sağladım.

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

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.