Вопрос

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

ОБНОВЛЯТЬ:я просто читал Последние новости Йегге когда я нашел этот самородок:

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

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

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

Это было полезно?

Решение 3

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

Другие советы

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

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

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

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

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

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

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

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

    }

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

package 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 был однопоточным, и, пожалуйста,

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 там, где нет параллелизма)

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

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

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

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

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

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

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

    ... etc.
}

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

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

Кэш 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.Я использовал PriorityQueue, который в основном работает как FIFO, а не потокобезопасный.Использовал компаратор на основе создания времени страницы и на основе выполнения упорядочения страниц на наименее недавно используемое время.

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

Страница добавлена ​​в кеш:2
Страница добавлена ​​в кеш:1
Страница добавлена ​​в кеш:0
Страница:2 уже есть в кеше.Время последнего доступа обновлено
Ошибка страницы, СТРАНИЦА:1, заменено на СТРАНИЦУ:8
Страница добавлена ​​в кеш:8
Страница:2 уже есть в кеше.Время последнего доступа обновлено
Ошибка страницы, СТРАНИЦА:0, заменено на СТРАНИЦА:4
Страница добавлена ​​в кеш:4

ВЫХОД

Страницы LRUCache
-------------
Имя страницы:8, время создания страницы:1365957019974
Имя страницы:2, время создания страницы:1365957020074
Имя страницы:4, время создания страницы: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;
    }
}

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

}

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

Однако я согласен с выводом Хэнка и принятым ответом - если бы я начал это сегодня снова, я бы проверил CacheBuilder в Guava.

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

}

Что касается кеша, вы, как правило, будете искать часть данных через прокси-объект (URL, String ....), поэтому для интерфейса вам понадобится карта. но чтобы выкинуть вещи, вам нужна очередь, такая как структура. Внутренне я бы поддерживал две структуры данных, Priority-Queue и 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 относительно карты.

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

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

Я ищу лучший кэш LRU с использованием кода Java. Можете ли вы поделиться своим кодом кэша Java LRU, используя LinkedHashMap и Collections # synchronizedMap ? В настоящее время я использую LRUMap реализует 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 (ключ объекта) и put (ключ объекта, значение объекта) вызывают вышеуказанный метод moveToFront .

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

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.

Еще одна мысль и даже простая реализация, использующая коллекцию Java LinkedHashMap.

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

У нас может быть pageno и содержимое страницы, в моем случае pageno является целым числом и содержанием страницы. Я сохранил строку значений номера страницы.

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 

Следуя концепции @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);
    }
}

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top