Как бы вы реализовали LRU-кеш в Java? - PullRequest
161 голосов
/ 21 октября 2008

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

ОБНОВЛЕНИЕ: я только что прочитал последние Yegge, когда я нашел этот самородок:

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

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

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

Ответы [ 21 ]

99 голосов
/ 23 декабря 2009

Мне нравятся многие из этих предложений, но сейчас я думаю, что я буду придерживаться 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));
18 голосов
/ 08 ноября 2010

Или используйте эту структуру данных Apache Commons:

https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/map/LRUMap.html

15 голосов
/ 13 декабря 2012

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

10 голосов
/ 18 декабря 2013

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

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

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

Первая не параллельная версия:

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 без флага true для конструктора просто реализует кэш 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, и затем я понял, что все, чего я действительно хотел был простой LRU. Так что проигнорируйте мой предыдущий пост и дайте мне знать, если вы хотите увидеть кэш LRU / LFU / FIFO, который можно переключать с помощью enum ... нет? Хорошо .. вот он.

Простейший из возможных 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 в качестве примера и добавить хэш-карту обратного просмотра от элемента к узлу, чтобы сделать операции удаления ОЧЕНЬ БЫСТРЕЕ и более согласованными. Я тоже начал, но потом понял, что мне это не нужно. Но ... может быть ...

Когда вызывается put , ключ добавляется в очередь. Когда вызывается 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. Причина, по которой я не всегда использую библиотеку, заключается в том, что я не всегда хочу, чтобы каждый war-файл занимал 80 МБ, и я пишу библиотеки, поэтому я стремлюсь сделать библиотеки подключаемыми с достаточно хорошим решением, а кто-то может -в другом провайдере кэша, если им нравится. :) Я никогда не знаю, когда кому-то может понадобиться 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.

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

9 голосов
/ 05 марта 2009

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 требуется только блокировка на начало и конец списка (для каждого региона). При удалении или получении весь регион должен быть заблокирован. Мне любопытно, могут ли здесь помочь какие-то списки связанных списков Atomic, вероятно, для главы списка. Может быть, больше.

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

  1. будет легче писать и быстрее при умеренном параллелизме.

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

8 голосов
/ 17 июля 2010

Существует две реализации с открытым исходным кодом.

Apache Solr имеет ConcurrentLRUCache: https://lucene.apache.org/solr/3_6_1/org/apache/solr/util/ConcurrentLRUCache.html

Существует проект с открытым исходным кодом для ConcurrentLinkedHashMap: http://code.google.com/p/concurrentlinkedhashmap/

7 голосов
/ 21 октября 2008

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

Элемент object будет оберткой для объектов в кеше:

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

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

    ... etc.
}
6 голосов
/ 28 октября 2014

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

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);
}
}
5 голосов
/ 06 апреля 2014

Кэш 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);
  }

}
3 голосов
/ 14 апреля 2013

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

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

Страница добавлена ​​в кеш: 2
Страница добавлена ​​в кеш: 1
Страница добавлена ​​в кеш: 0
Страница: 2 уже существует в кеше. Время последнего доступа обновлено
Ошибка страницы, СТРАНИЦА: 1, Заменена на СТРАНИЦУ: 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;
    }
}
...