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

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

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

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

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

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

Ответы [ 21 ]

2 голосов
/ 21 февраля 2013

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

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

Гуавы.
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;


public class MaxIdleLRUCache<KK, VV> {

    final static private int IDEAL_MAX_CACHE_ENTRIES = 128;

    public interface DeadElementCallback<KK, VV> {
        public void notify(KK key, VV element);
    }

    private Object lock = new Object();
    private long minAge;
    private HashMap<KK, Item<VV>> cache;


    public MaxIdleLRUCache(long minAgeMilliseconds) {
        this(minAgeMilliseconds, IDEAL_MAX_CACHE_ENTRIES);
    }

    public MaxIdleLRUCache(long minAgeMilliseconds, int idealMaxCacheEntries) {
        this(minAgeMilliseconds, idealMaxCacheEntries, null);
    }

    public MaxIdleLRUCache(long minAgeMilliseconds, int idealMaxCacheEntries, final DeadElementCallback<KK, VV> callback) {
        this.minAge = minAgeMilliseconds;
        this.cache = new LinkedHashMap<KK, Item<VV>>(IDEAL_MAX_CACHE_ENTRIES + 1, .75F, true) {
            private static final long serialVersionUID = 1L;

            // This method is called just after a new entry has been added
            public boolean removeEldestEntry(Map.Entry<KK, Item<VV>> eldest) {
                // let's see if the oldest entry is old enough to be deleted. We don't actually care about the cache size.
                long age = System.currentTimeMillis() - eldest.getValue().birth;
                if (age > MaxIdleLRUCache.this.minAge) {
                    if ( callback != null ) {
                        callback.notify(eldest.getKey(), eldest.getValue().payload);
                    }
                    return true; // remove it
                }
                return false; // don't remove this element
            }
        };

    }

    public void put(KK key, VV value) {
        synchronized ( lock ) {
//          System.out.println("put->"+key+","+value);
            cache.put(key, new Item<VV>(value));
        }
    }

    public VV get(KK key) {
        synchronized ( lock ) {
//          System.out.println("get->"+key);
            Item<VV> item = getItem(key);
            return item == null ? null : item.payload;
        }
    }

    public VV remove(String key) {
        synchronized ( lock ) {
//          System.out.println("remove->"+key);
            Item<VV> item =  cache.remove(key);
            if ( item != null ) {
                return item.payload;
            } else {
                return null;
            }
        }
    }

    public int size() {
        synchronized ( lock ) {
            return cache.size();
        }
    }

    private Item<VV> getItem(KK key) {
        Item<VV> item = cache.get(key);
        if (item == null) {
            return null;
        }
        item.touch(); // idle the item to reset the timeout threshold
        return item;
    }

    private static class Item<T> {
        long birth;
        T payload;

        Item(T payload) {
            this.birth = System.currentTimeMillis();
            this.payload = payload;
        }

        public void touch() {
            this.birth = System.currentTimeMillis();
        }
    }

}
2 голосов
/ 26 мая 2011

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

}

2 голосов
/ 22 октября 2008

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

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

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

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

1 голос
/ 25 мая 2011

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

package util.collection;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Limited size concurrent cache map implementation.<br/>
 * LRU: Least Recently Used.<br/>
 * If you add a new key-value pair to this cache after the maximum size has been exceeded,
 * the oldest key-value pair will be removed before adding.
 */

public class ConcurrentLRUCache<Key, Value> {

private final int maxSize;
private int currentSize = 0;

private ConcurrentHashMap<Key, Value> map;
private ConcurrentLinkedQueue<Key> queue;

public ConcurrentLRUCache(final int maxSize) {
    this.maxSize = maxSize;
    map = new ConcurrentHashMap<Key, Value>(maxSize);
    queue = new ConcurrentLinkedQueue<Key>();
}

private synchronized void freeSpace() {
    Key key = queue.poll();
    if (null != key) {
        map.remove(key);
        currentSize = map.size();
    }
}

public void put(Key key, Value val) {
    if (map.containsKey(key)) {// just heat up that item
        put(key, val);
        return;
    }
    while (currentSize >= maxSize) {
        freeSpace();
    }
    synchronized(this) {
        queue.add(key);
        map.put(key, val);
        currentSize++;
    }
}

public Value get(Key key) {
    return map.get(key);
}
}
1 голос
/ 21 декабря 2011

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

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

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

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

0 голосов
/ 30 января 2019

Следуя концепции @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);
    }
}
0 голосов
/ 10 августа 2010

Я ищу лучший кэш LRU с использованием кода Java. Можно ли поделиться кодом кеша Java LRU, используя LinkedHashMap и Collections#synchronizedMap? В настоящее время я использую LRUMap implements Map, и код работает нормально, но я получаю ArrayIndexOutofBoundException при нагрузочном тестировании, используя 500 пользователей по приведенному ниже методу Метод перемещает последний объект в начало очереди.

private void moveToFront(int index) {
        if (listHead != index) {
            int thisNext = nextElement[index];
            int thisPrev = prevElement[index];
            nextElement[thisPrev] = thisNext;
            if (thisNext >= 0) {
                prevElement[thisNext] = thisPrev;
            } else {
                listTail = thisPrev;
            }
            //old listHead and new listHead say new is 1 and old was 0 then prev[1]= 1 is the head now so no previ so -1
            // prev[0 old head] = new head right ; next[new head] = old head
            prevElement[index] = -1;
            nextElement[index] = listHead;
            prevElement[listHead] = index;
            listHead = index;
        }
    }
Методы

get(Object key) и put(Object key, Object value) вызывают вышеуказанный метод moveToFront.

0 голосов
/ 08 августа 2012

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

LinkedHashMap также поддерживает порядок доступа на основе параметра, переданного в его конструкторе. Он поддерживает список с двойной линией для поддержания порядка (см. LinkedHashMap.Entry)

@ Pacerier правильно, что LinkedHashMap сохраняет тот же порядок во время итерации, если элемент добавляется снова, но только в случае режима порядка вставки.

это то, что я нашел в документации по Java объекта LinkedHashMap.Entry

    /**
     * This method is invoked by the superclass whenever the value
     * of a pre-existing entry is read by Map.get or modified by Map.set.
     * If the enclosing Map is access-ordered, it moves the entry
     * to the end of the list; otherwise, it does nothing.
     */
    void recordAccess(HashMap<K,V> m) {
        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        if (lm.accessOrder) {
            lm.modCount++;
            remove();
            addBefore(lm.header);
        }
    }

этот метод заботится о перемещении недавно использованного элемента в конец списка. В общем, LinkedHashMap - лучшая структура данных для реализации LRUCache.

0 голосов
/ 18 сентября 2014

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

...