Як би ви реалізували кеш LRU на Java?


169

Будь ласка, не кажіть EHCache чи OSCache тощо. Припустімо для цілей цього питання, що я хочу реалізувати свій власний, використовуючи лише SDK (навчання на практиці). Враховуючи, що кеш буде використовуватися в багатопотоковому середовищі, які структури даних ви б використовували? Я вже реалізував один із використанням LinkedHashMap та Collections # sinkronizedMap , але мені цікаво, чи будь-яка з нових одночасних колекцій буде кращими кандидатами.

ОНОВЛЕННЯ: Я щойно читав останні новини від Yegge, коли знайшов цей самородок:

Якщо вам потрібен постійний доступ і хочете підтримувати порядок вставки, ви не можете зробити краще, ніж LinkedHashMap, справді чудова структура даних. Єдиний спосіб, який міг би бути чудовішим, - якби існувала паралельна версія. Але на жаль.

Я думав майже про те саме, перш ніж піти з реалізацією LinkedHashMap+, про яку Collections#synchronizedMapя згадував вище. Приємно знати, що я не раз щось не помічав.

Виходячи з відповідей на даний момент, це здається, що найкращим моїм ставкою для дуже одночасного LRU було б розширення ConcurrentHashMap, використовуючи ту саму логіку, що і LinkedHashMapвикористовує.



Дуже схоже запитання і тут
Mifeet

Відповіді:


102

Мені подобається багато цих пропозицій, але наразі я думаю, що я буду дотримуватися LinkedHashMap+ Collections.synchronizedMap. Якщо я повернутися до цього в майбутньому, я , ймовірно , працювати над розширенням ConcurrentHashMapв тому ж чином LinkedHashMapпроходить 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
Однак я хотів би використовувати інкапсуляцію тут, а не спадщину. Це те, що я навчився з ефективної Java.
Капіль Д

10
@KapilD Минув час, але я майже впевнений, що JavaDocs LinkedHashMapявно схвалює цей метод для створення реалізації LRU.
Генк Гей

7
@HankGay LinkedHashMap Java (з третім параметром = true) не є кешем LRU. Це пояснюється тим, що повторне введення запису не впливає на порядок записів (справжній кеш LRU розмістить останній вставлений запис на звороті ітераційного порядку, незалежно від того, що цей запис спочатку існує в кеші)
Pacerier

2
@Pacerier Я взагалі не бачу такої поведінки. Якщо карта AccessOrder увімкнена, усі дії вносять запис як останній раз (найсвіжіший): початкове вставлення, оновлення значення та отримання значення. Я щось пропускаю?
Есаїлія

3
@Pacerier "повторне введення запису не впливає на порядок записів", це неправильно. Якщо ви подивитеся на реалізацію LinkedHashMap, для методу "put" він успадковує реалізацію від HashMap. І Javadoc HashMap каже: "Якщо раніше карта містила відображення ключа, старе значення замінюється". І якщо ви перевірите його вихідний код, при заміні старого значення він викличе метод recordAccess, а в методі recordAccess в LinkedHashMap це виглядає приблизно так: якщо (lm.accessOrder) {lm.modCount ++; видалити (); addBefore (lm.header);}
nybon


10

Це другий раунд.

Перший раунд був те, що я придумав, тоді я перечитав коментарі з доменом трохи більше вбудовані в моїй голові.

Тож ось найпростіша версія з одиничним тестом, яка показує, що вона працює на основі деяких інших версій.

Спочатку не сумісна версія:

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


}

Справжній прапор буде відслідковувати доступ і отримує. Див. JavaDocs. RemoveEdelstEntry без справжнього прапора для конструктора просто реалізує кеш FIFO (див. Примітки нижче про FIFO та deleteEldestEntry).

Ось тест, який доводить, що він працює як кеш 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 ();



    }

}

Це остання публікація .. Перший пост я видалив, оскільки це був LFU, а не кеш LRU.

Я думав, що піду ще раз. Я намагався створити найпростішу версію кешу LRU, використовуючи стандартний JDK без занадто великої реалізації.

Ось що я придумав. Перша моя спроба була трохи катастрофою, коли я застосував LFU замість LRU, а потім додав до нього підтримку FIFO та LRU ... і тоді я зрозумів, що це стає монстром. Тоді я почав розмовляти зі своїм приятелем Джоном, який ледве зацікавився, а потім я дуже глибоко описав, як я реалізував LFU, LRU і FIFO і як ви можете переключити його простим аргументом ENUM, і тоді я зрозумів, що все, що мені дуже хочеться була простою LRU. Тому ігноруйте попередній пост від мене, і повідомте мені, чи хочете ви побачити кеш LRU / LFU / FIFO, який можна перемикати через перерахунок ... ні? Добре .. ось він і піде.

Найпростіший можливий LRU, що використовує лише JDK. Я реалізував як одночасну, так і неконкурентну версію.

Я створив загальний інтерфейс (це мінімалізм, імовірно, не вистачає декількох функцій, які б вам хотілося, але він працює для моїх випадків використання, але нехай, якщо ви хочете побачити функцію 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 як приклад та додати хеш-карту зворотного пошуку від елемента до вузла, щоб зробити операції з видалення ДУЖЕ швидше та послідовніші. Я теж почав, але потім зрозумів, що мені це не потрібно. Але ... можливо ...

Коли виклик ставиться , ключ додається до черги. Коли викликається get , ключ видаляється та знову додається до верхньої частини черги.

Якщо ваш кеш невеликий, а елемент будівлі дорогий, це повинен бути хорошим кешем. Якщо ваш кеш дійсно великий, то лінійний пошук може бути шийкою пляшки, особливо якщо у вас немає гарячих областей кешу. Чим інтенсивніші гарячі точки, тим швидший лінійний пошук, оскільки гарячі елементи завжди знаходяться у верхній частині лінійного пошуку. У будь-якому випадку ... для цього потрібно швидше написати інший LinkedList, який має операцію видалення, яка має зворотний елемент для пошуку вузла для видалення, тоді видалення буде приблизно таким же швидким, як видалення ключа з хеш-карти.

Якщо у вас є кеш-пам'ять під 1000 елементів, це має спрацювати нормально.

Ось простий тест, щоб показати його дії в дії.

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

Основні відмінності полягають у використанні ConcurrentHashMap замість HashMap та застосуванні Lock (я міг би відійти від синхронізованого, але ...).

Я не перевіряв його під обстрілом, але це здається простим кешем LRU, який може працювати в 80% випадків використання, коли вам потрібна проста карта LRU.

Я вітаю відгуки, за винятком того, чому ви не використовуєте бібліотеки a, b або c. Причиною того, що я не завжди використовую бібліотеку, є те, що я не завжди хочу, щоб кожен файл війни був 80 Мб, і я пишу бібліотеки, тому я прагну зробити так, щоб лібуси підключалися з достатньо хорошим рішенням на місці, і хтось може підключити -в іншому постачальнику кеш-пам'яті, якщо їм це подобається. :) Я ніколи не знаю, коли комусь може знадобитися Guava або ehcache або щось інше, я не хочу їх включати, але якщо я зробить кешування плагіном, я також не виключаю їх.

Зниження залежностей має свою винагороду. Я люблю отримувати відгуки про те, як зробити це ще простішим, швидшим або обом.

Також, якщо хтось знає, що готовий поїхати….

Гаразд .. Я знаю, що ти думаєш ... Чому він просто не використовує deleteEldest із LinkedHashMap, і добре, що я повинен, але .... але .. але .. Це був би FIFO, а не LRU, і ми були намагаються реалізувати 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 ();

Отже, ось швидкий і брудний кеш-пам'ять FIFO, використовуючи deleteEldestEntry.

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 швидко. Немає пошуків навколо. Ви можете перейти до FIFO перед LRU, і це дозволило б обробляти більшість гарячих записів досить чудово. Кращому LRU знадобиться цей зворотний елемент для функції Node.

У будь-якому випадку ... тепер, коли я написав якийсь код, дозвольте мені переглянути інші відповіді та побачити, що я пропустив ... вперше сканував їх.


9

LinkedHashMapє O (1), але вимагає синхронізації. Там не потрібно винаходити колесо.

2 варіанти підвищення одночасності:

1. Створіть кілька LinkedHashMap, і хеш в них: приклад: LinkedHashMap[4], index 0, 1, 2, 3. На клавіші зробіть key%4 (або binary ORввімкніть [key, 3]), щоб вибрати, яку карту потрібно зробити / поставити / видалити.

2. Ви можете зробити "майже" LRU, розширивши ConcurrentHashMapта маючи пов'язану структуру хеш-карти, як у кожному з регіонів всередині неї. Блокування відбувається більш детально, ніж LinkedHashMapсинхронізоване. На putабо putIfAbsentтільки замок на голові і хвості списку потрібно (по регіонах). Після видалення або отримання весь регіон потрібно заблокувати. Мені цікаво, якщо тут можуть допомогти якісь пов'язані з атомними списками - напевно, так і для голови списку. Можливо, для більше.

Структура не підтримуватиме загальний порядок, а лише порядок на один регіон. Поки кількість записів набагато більша за кількість регіонів, це досить добре для більшості кеш-запитів. Кожен регіон повинен мати свій власний підрахунок вступу, це буде використовуватись, а не глобальний підрахунок для тригера виселення. Кількість регіонів за замовчуванням ConcurrentHashMapстановить 16, що для більшості серверів сьогодні достатньо.

  1. було б легше писати і швидше за помірної одночасності.

  2. було б складніше написати, але масштаб набагато краще при дуже високій одночасності. Це було б повільніше для нормального доступу (так само ConcurrentHashMapповільніше, ніж HashMapтам, де немає одночасності)


8

Є дві реалізації з відкритим кодом.

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з Гуави, але я його не помітив у документах. Будь-яка ідея, що відбувається з цим зусиллям?
Хенк Гей

3
Спрощена версія була інтегрована, але тести ще не завершені, тому вона ще не є загальнодоступною. У мене було багато проблем із глибшою інтеграцією, але я сподіваюся закінчити це, оскільки є деякі приємні алгоритмічні властивості. Можливість прослуховування виселення (ємність, термін придатності, GC) була додана і заснована на підході CLHM (черга слухачів). Я також хотів би внести ідею "зважених значень", оскільки це корисно під час кешування колекцій. На жаль, через інші зобов’язання я був занадто заболочений, щоб присвятити час, який заслуговує Гуава (і що я обіцяв Кевіну / Чарльзу).
Бен Манес

3
Оновлення: Інтеграція завершена та відкрита у Guava r08. Це через налаштування #maximumSize ().
Бен Манес

7

Я б розглядав можливість використання java.util.concurrent.PriorityBlockingQueue , з пріоритетом визначається лічильником "numberOfUses" у кожному елементі. Я був би дуже, дуже обережний, щоб виправити всі мої синхронізації, оскільки лічильник "numberOfUses" означає, що елемент не може бути незмінним.

Об'єкт елемента буде обгорткою для об'єктів у кеші:

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

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

    ... etc.
}

Ви не маєте на увазі, повинні бути незмінними?
shsteimer

2
зауважте, що якщо ви спробуєте виконати версію prioblockingqueue, згадану Стівом mcleod, вам слід зробити елемент незмінним, оскільки зміна елемента, який перебуває в черзі, не вплине, вам потрібно буде видалити елемент і знову додати його для того, щоб переокреміть його.
Джеймс

Джеймс нижче вказує на помилку, яку я зробив. Я пропоную як доказ того, наскільки важко кровоточити, - писати надійні, міцні схованки.
Стів МакЛеод

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?
Аквель

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

Будь ласка, виправте це. Перш за все, він не компілюється на рядку map.containsKey (ключ). По-друге, у get () ви повинні перевірити, чи дійсно було видалено ключ, інакше карта і черга не синхронізуються, а "queue.size ()> = size" завжди стає істинним. Я опублікую свою версію з виправленням, як мені сподобалася ваша ідея використання цих двох колекцій.
Олександр Лех

3

Ось моя реалізація для LRU. Я використовував PriorityQueue, який в основному працює як FIFO, а не безпечний. Використовуваний компаратор на основі створення часу на сторінці та на основі впорядкування сторінок за найменший останній час.

Сторінки для розгляду: 2, 1, 0, 2, 8, 2, 4

Сторінка, додана в кеш-пам'ять, є: 2
Сторінка, що додається в кеш, - це: 1
Сторінка, додана в кеш, - це: 0
Сторінка: 2 вже є в кеші. Останнє оновлення часу оновлення
сторінки Помилка сторінки, Сторінка: 1, замінена на PAGE: 8
Сторінка, додана в кеш, - це 8
сторінок: 2 вже є в кеші. Останнє оновлення часу
помилки сторінки, Сторінка: 0, замінено на PAGE: 4
Сторінка додана в кеш-пам'ять: 4

ВИХІД

Сторінки LRUCache
------------- Назва сторінки
: 8, PageCreationTime: 1365957019974
Ім'я сторінки: 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 .... ви не впоралися з однією ситуацією .. що робити, якщо один і той же об'єкт використовується кілька разів? у цьому випадку ми не повинні додавати кілька записів для одного об’єкта ... натомість його ключ повинен бути

5
Попередження: Це не кеш LRU. У кеш-пам’яті LRU ви викидаєте елементи, щонайменше недавно доступні. Цей викидає найменше нещодавно написане. Це також лінійне сканування для виконання операції queue.remove (key).
Дейв Л.

Також 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, String ....), так що для інтерфейсу ви хочете отримати карту. але для того, щоб вигнати речі, ви хочете структуру, схожу на чергу. Внутрішньо я б підтримував дві структури даних, пріоритетну чергу та HashMap. ось реалізація, яка повинна мати можливість робити все за 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 = хвіст; } Коли цей код буде виконаний? Я провів кілька сухих пробіжок і думаю, що це ніколи не буде виконано, і його слід усунути.
Діпеш

1

Погляньте на ConcurrentSkipListMap . Він повинен дати вам час (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

1

Ось моя власна реалізація цієї проблеми

simplelrucache забезпечує безпечний потік, дуже простий, нерозподілений керування LRU з підтримкою TTL. Він передбачає дві реалізації:

  • Паралельний на основі 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

Я шукаю кращий кеш LRU за допомогою коду Java. Чи можливо вам поділитися кодом кешу Java LRU за допомогою LinkedHashMapта 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 зберігає той самий порядок під час ітерації, якщо елемент знову додається, але це лише у режимі порядку вставки.

це те, що я знайшов у документах Java на об’єкт LinkedHashMap.Entry

    /**
     * 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 надав метод deleteEldestEntry, який можна змінити способом, зазначеним у прикладі. За замовчуванням реалізація цієї структури колекції є помилковою. Якщо справжня і розмір цієї структури перевищує початкову потужність, то найстарші або старіші елементи будуть видалені.

У нас може бути вміст сторінки та сторінки, у моєму випадку pageno є цілим числом та pagecontent, я зберігав рядок значень номера сторінки.

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

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


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


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

           private static final long serialVersionUID = 1L;

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

                     eldest) {
                          return size() > CACHE_SIZE;
                     }

                };

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

     }

}

Результат виконання вищевказаного коду такий:

 Pages for consideration : 2, 1, 0, 2, 8, 2, 4,99
--------------------------------------------------
    {2=2, 1=1, 0=0}  , After first 3 pages in cache
    {2=2, 1=1, 0=0}  , Page 2 became the latest page in the cache
    {1=1, 0=0, 8=8}  , Adding page 8, which removes eldest element 2 
    {0=0, 8=8, 2=2}  , Page 2 became the latest page in the cache
    {8=8, 2=2, 4=4}  , Adding page 4, which removes eldest element 1 
    {2=2, 4=4, 99=99} , Adding page 99, which removes eldest element 8 

Це FIFO. Він попросив LRU.
RickHigh

Не вдалося виконати цей тест ... cache.get (2); cache.get (3); cache.put (6, 6); cache.put (7, 7); ок | = cache.size () == 4 || die ("розмір" + cache.size ()); ок | = cache.getSilent (2) == 2 || die (); ок | = cache.getSilent (3) == 3 || die (); ок | = cache.getSilent (4) == null || die (); ок | = cache.getSilent (5) == null || die ();
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);
    }
}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.