Как бы вы реализовали кеш LRU в Java?

Пожалуйста, не говорите EHCache или OSCache и т.д. Предположим для целей этого вопроса, что я хочу реализовать свои собственные, используя только SDK (обучение на практике). Учитывая, что кеш будет использоваться в многопоточной среде, какие datastructures вы использовали бы? Я уже реализовал один, используя LinkedHashMap и Коллекции # synchronizedMap, но мне любопытно, будут ли какие-либо из новых параллельных коллекций лучшими кандидатами.

UPDATE: я просто читал Yegge last, когда нашел этот самородок:

Если вам нужен постоянный доступ и вы хотите сохранить порядок вставки, вы не сможете сделать лучше, чем LinkedHashMap, действительно замечательная структура данных. Единственный способ, возможно, более замечательный, если бы существовала параллельная версия. Но, увы.

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

Основываясь на ответах до сих пор, мне кажется, что мой лучший выбор для высококонкурентного LRU будет заключаться в расширении ConcurrentHashMap, используя некоторые из та же логика, что и LinkedHashMap.

Ответ 1

Если бы я делал это снова с нуля сегодня, я бы использовал Guava CacheBuilder.

Ответ 2

Мне нравятся многие из этих предложений, но сейчас я думаю, что буду придерживаться LinkedHashMap + Collections.synchronizedMap. Если я повторю это в будущем, я, вероятно, буду работать над расширением ConcurrentHashMap таким же образом LinkedHashMap extends HashMap.

UPDATE:

По запросу, здесь суть моей текущей реализации.

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

Ответ 4

Это раунд два.

Первый раунд был тем, что я придумал, а затем перечитал комментарии с доменом, немного укоренившимся в моей голове.

Итак, вот простейшая версия с unit test, которая показывает, что она работает на некоторых других версиях.

Сначала неконкурентная версия:

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


}

Истинный флаг будет отслеживать доступ к get и puts. См. JavaDocs. RemoveEdelstEntry без истинного флага для конструктора просто реализует кеш FIFO (см. Примечания ниже на FIFO и removeEldestEntry).

Вот тест, который доказывает, что он работает как кеш LRU:

public class LruSimpleTest {

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


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

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


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


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


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


        if ( !ok ) die ();

    }

Теперь для параллельной версии...

пакет org.boon.cache;

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

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

    final CacheMap<K, V>[] cacheRegions;


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

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

        }

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


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

            V old;
            try {

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

        }


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

            try {

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

        @Override
        public V remove ( Object key ) {

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

            try {

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

        }

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

            V value;

            try {

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

        }

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

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


    }

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

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

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

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

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

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

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

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

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

    }

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

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

    public String toString () {

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

        return builder.toString ();
    }


}

Вы можете понять, почему я сначала закрываю неконкурентную версию. Вышеупомянутые попытки создать некоторые полосы для уменьшения блокировки. Таким образом, мы хешируем ключ, а затем просматриваем этот хэш, чтобы найти фактический кеш. Это делает размер ограничения более предположениями/приблизительными предположениями в пределах достаточной суммы ошибок в зависимости от того, насколько хорошо распространен алгоритм хеширования ключей.

Вот тест, показывающий, что параллельная версия, вероятно, работает.:) (Тест под огнем был бы реальным путем).

public class SimpleConcurrentLRUCache {


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


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

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


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


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

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


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

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

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


        puts (cache);


        if ( !ok ) die ();

    }


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


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

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


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

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

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



    }

}

Это последнее сообщение. Первое сообщение, которое я удалил, поскольку это был LFU, а не кеш LRU.

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

Вот что я придумал. Моя первая попытка была немного катастрофой, поскольку я реализовал LFU вместо LRU, а затем добавил FIFO и поддержку LRU... и тогда я понял, что это становится монстром. Затем я начал разговаривать с моим приятелем Джоном, который был едва заинтересован, и затем я подробно описал, как я реализовал LFU, LRU и FIFO, и как вы могли переключить его с помощью простого ENUM arg, а затем я понял, что все, что я действительно хотел был простой 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 и добавить хеш-карту обратного поиска с элемента на node, чтобы сделать операции удаления LOT FASTER и более согласованными. Я тоже начал, но потом понял, что мне это не нужно. Но... может быть...

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

Если ваш кеш мал и здание дорого стоит, это должно быть хорошим кешем. Если ваш кеш действительно большой, то линейный поиск может быть шеей бутылки, особенно если у вас нет горячих областей кеша. Чем интенсивнее горячие точки, тем быстрее линейный поиск в качестве горячих элементов всегда находится в верхней части линейного поиска. В любом случае... для этого нужно быстрее написать другой LinkedList, который имеет операцию удаления, которая имеет обратный элемент для поиска node для удаления, тогда удаление будет примерно таким же быстрым, как удаление ключа из хэш-карты.

Если у вас есть кеш менее 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 МБ, и я пишу библиотеки, поэтому я стараюсь, чтобы libs подключался с достаточным решением, и кто-то может подключиться - в другом провайдере кешей, если им нравится.:) Я никогда не знаю, когда кому-то понадобится Guava или ehcache или что-то еще, я не хочу их включать, но если я сделаю кеширование подключаемым модулем, я тоже их не исключу.

Сокращение зависимостей имеет свою собственную награду. Мне нравится получать некоторые отзывы о том, как сделать это еще проще или быстрее, или и то, и другое.

Также, если кто-то знает о готовности идти....

Хорошо.. Я знаю, о чем вы думаете... Почему он не просто использует removeEldest запись из 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 с использованием removeEldestEntry.

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.

В любом случае... теперь, когда я написал код, позвольте мне пройти через другие ответы и посмотреть, что я пропустил... в первый раз, когда я их просмотрел.

Ответ 5

LinkedHashMap - O (1), но требует синхронизации. Нет необходимости изобретать велосипед там.

2 варианта увеличения concurrency:

1. Создайте в них несколько LinkedHashMap и хэш: Пример: LinkedHashMap[4], index 0, 1, 2, 3. На клавише do key%4 (или binary OR on [key, 3]) выберите, какую карту сделать put/get/remove.

2. Вы можете сделать "почти" LRU, расширив ConcurrentHashMap и имея связанную карту хэша, такую ​​как структура в каждой из областей внутри нее. Блокировка будет происходить более подробно, чем LinkedHashMap, которая синхронизируется. На a put или putIfAbsent требуется только блокировка на голове и хвосте списка (по региону). При удалении или захвате весь регион должен быть заблокирован. Мне любопытно, могут ли здесь быть использованы связанные с Atomic списки, возможно, для главы списка. Может быть, для большего.

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

  • было бы легче писать и быстрее при умеренном concurrency.

  • было бы сложнее писать, но масштабировать намного лучше при очень высоком concurrency. Это будет медленнее для обычного доступа (так же, как ConcurrentHashMap работает медленнее, чем HashMap, где нет concurrency)

Ответ 7

Я хотел бы использовать java.util.concurrent.PriorityBlockingQueue, причем приоритет определяется счетчиком "numberOfUses" в каждом элементе. Я был бы очень сильным, очень осторожным, чтобы получить правильную синхронизацию, так как счетчик numberOfUses подразумевает, что элемент не может быть неизменным.

Элемент-элемент будет оболочкой для объектов в кеше:

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

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

    ... etc.
}

Ответ 8

Надеюсь, что это поможет.

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

Ответ 9

LRU Cache может быть реализован с использованием 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);
  }

}

Ответ 10

Вот моя реализация для LRU. Я использовал PriorityQueue, который в основном работает как FIFO, а не threadafe. Используемый компаратор на основе создания времени страницы и на основе выполнения заказа страниц для наименее недавно использованного времени.

Страницы для рассмотрения: 2, 1, 0, 2, 8, 2, 4

Страница добавлена ​​в кеш: 2
Страница добавлена ​​в кеш: 1
Страница добавлена ​​в кеш: 0
Страница: 2 уже существует в кеше. Последнее доступное время обновлено
Ошибка страницы, PAGE: 1, заменена на PAGE: 8
Страница добавлена ​​в кеш: 8
Страница: 2 уже существует в кеше. Последнее доступное время обновлено
Ошибка страницы, PAGE: 0, заменена на PAGE: 4
Страница добавлена ​​в кеш: 4

OUTPUT

Страницы LRUCache
-------------
PageName: 8, PageCreationTime: 1365957019974
PageName: 2, PageCreationTime: 1365957020074
PageName: 4, PageCreationTime: 1365957020174

введите здесь код

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


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

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

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

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


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

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

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

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

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

    }

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

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

    public String getPageName() {
        return pageName;
    }

    public long getPageCreationTime() {
        return pageCreationTime;
    }

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

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

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

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


class LRUPageComparator implements Comparator<LRUPage>{

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

Ответ 11

Ну, для кэша вы обычно будете искать часть данных через прокси-объект (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 относительно карты.

Ответ 12

Вот моя проверенная наиболее эффективная параллельная реализация кэша 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);
}

}

Ответ 13

Это кеш LRU, который я использую, который инкапсулирует LinkedHashMap и обрабатывает concurrency с простой блокировкой синхронизации, защищающей сочные пятна. Он "касается" элементов, поскольку они используются так, что они снова становятся "самым свежим" элементом, так что это фактически LRU. У меня также было требование, чтобы мои элементы имели минимальный срок службы, что вы также можете считать "максимально допустимым", тогда вы должны выселить.

Однако я согласен с заключением Хэнка и принятым ответом - если бы я начал это снова сегодня, я бы проверил Guava 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 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();
        }
    }

}

Ответ 14

Посмотрите ConcurrentSkipListMap. Это должно дать вам время log (n) для тестирования и удаления элемента, если оно уже содержится в кеше, и постоянное время для его повторного добавления.

Вам просто понадобится некоторый счетчик и т.д. и элемент оболочки, чтобы заставить упорядочить порядок LRU и обеспечить, чтобы последние материалы были отброшены, когда кеш заполнен.

Ответ 15

Вот моя краткая реализация, пожалуйста, критикуйте или улучшите ее!

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

Ответ 16

Вот моя собственная реализация этой проблемы

simplelrucache обеспечивает потокобезопасное, очень простое, нераспространенное кэширование LRU с поддержкой TTL. Он обеспечивает две реализации:

  • Параллельно на основе ConcurrentLinkedHashMap
  • Синхронизировано на основе LinkedHashMap

Вы можете найти его здесь: http://code.google.com/p/simplelrucache/

Ответ 17

Я ищу лучший кеш 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.

Ответ 18

Хотелось бы добавить комментарий к ответу, данному Хэнком, но некоторые из того, как я не могу - пожалуйста, рассматривайте его как комментарий

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.

Ответ 19

Другая мысль и даже простая реализация с использованием коллекции LinkedHashMap Java.

LinkedHashMap предоставил метод removeEldestEntry и который может быть переопределен способом, указанным в примере. По умолчанию реализация этой структуры коллекции ложна. Если его истина и размер этой структуры не превышают начальную емкость, чем будут удалены старые или старые элементы.

Мы можем иметь pageno и содержимое страницы в моем случае 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 

Ответ 20

Следуя концепции @sanjanab (но после исправлений), я создал свою версию LRUCache, предоставив также Consumer, который позволяет при необходимости что-то делать с удаленными элементами.

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

Ответ 21

Android предлагает реализацию LRU Cache. код является чистым и понятным.