Como você implementaria um cache LRU em Java?

169

Por favor, não diga EHCache ou OSCache, etc. Suponhamos, para fins desta pergunta, que eu queira implementar meu próprio usando apenas o SDK (aprendendo fazendo). Dado que o cache será usado em um ambiente multithread, quais estruturas de dados você usaria? Já implementei um usando o LinkedHashMap e o Collections # synchronizedMap , mas estou curioso para saber se alguma das novas coleções simultâneas seria uma candidata melhor.

UPDATE: Eu estava lendo as últimas notícias de Yegge quando encontrei esta pepita:

Se você precisa de acesso em tempo constante e deseja manter o pedido de inserção, não pode fazer melhor do que um LinkedHashMap, uma estrutura de dados verdadeiramente maravilhosa. A única maneira que poderia ser mais maravilhosa é se houvesse uma versão simultânea. Mas infelizmente.

Eu estava pensando quase exatamente a mesma coisa antes de iniciar a implementação LinkedHashMap+ Collections#synchronizedMapmencionada acima. É bom saber que eu não tinha apenas esquecido algo.

Com base nas respostas até agora, parece que minha melhor aposta para uma LRU altamente simultânea seria estender o ConcurrentHashMap usando algumas das mesmas lógicas LinkedHashMapusadas.

Hank Gay
fonte
O(1)versão exigida: stackoverflow.com/questions/23772102/…
Ciro Santilli escreveu:
Muito pergunta semelhante também aqui
Mifeet

Respostas:

102

Eu gosto de muitas dessas sugestões, mas por enquanto acho que vou ficar com o LinkedHashMap+ Collections.synchronizedMap. Se eu revisitar isso no futuro, provavelmente trabalharei na extensão ConcurrentHashMapda mesma maneira que ela LinkedHashMapse estende HashMap.

ATUALIZAR:

Por solicitação, aqui está a essência da minha implementação atual.

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));
Hank Gay
fonte
15
No entanto, eu gostaria de usar o encapsulamento aqui em vez da herança. Isso é algo que aprendi com o Java eficaz.
Kapil D
10
@ KapilD Já faz um tempo, mas tenho quase certeza de que os JavaDocs LinkedHashMapendossam explicitamente esse método para criar uma implementação de LRU.
Hank Gay
7
O LinkedHashMap do Java @HankGay (com terceiro parâmetro = true) não é um cache LRU. Isso ocorre porque a reposição de uma entrada não afeta a ordem das entradas (um cache LRU real colocará a última entrada inserida na parte de trás da ordem de iteração, independentemente de essa entrada existir inicialmente no cache)
Pacerier
2
@ Pacerier Não vejo esse comportamento. Com o mapa ativado para accessOrder, todas as ações fazem uma entrada como usada mais recentemente (mais recente): inserção inicial, atualização e recuperação de valor. Estou esquecendo de algo?
Esailija
3
@Pacerier "re-colocar uma entrada não afeta a ordem das entradas", isso está incorreto. Se você examinar a implementação do LinkedHashMap, o método "put" herdará a implementação do HashMap. E o Javadoc do HashMap diz "Se o mapa continha anteriormente um mapeamento para a chave, o valor antigo é substituído". E se você verificar o código-fonte, ao substituir o valor antigo, ele chamará o método recordAccess e, no método recordAccess do LinkedHashMap, ficará assim: if (lm.accessOrder) {lm.modCount ++; remover(); addBefore (lm.header);}
nybon 30/07/2013
18

Se eu estivesse fazendo isso novamente do zero hoje, usaria goiaba CacheBuilder.

Hank Gay
fonte
10

Esta é a segunda rodada.

A primeira rodada foi a que eu criei e depois reli os comentários com o domínio um pouco mais arraigado na minha cabeça.

Então, aqui está a versão mais simples, com um teste de unidade que mostra que ele funciona com base em outras versões.

Primeiro a versão não simultânea:

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


}

A flag verdadeira rastreará o acesso de recebe e coloca. Veja JavaDocs. O removeEdelstEntry sem o sinalizador true para o construtor implementaria apenas um cache FIFO (consulte as notas abaixo em FIFO e removeEldestEntry).

Aqui está o teste que prova que funciona como um cache 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 ();

    }

Agora, a versão simultânea ...

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


}

Você pode ver por que eu abro a versão não simultânea primeiro. O acima tenta criar algumas faixas para reduzir a contenção de bloqueio. Então, nós hashes a chave e, em seguida, procuramos esse hash para encontrar o cache real. Isso faz com que o tamanho limite seja mais uma sugestão / palpite aproximado, com uma quantidade razoável de erros, dependendo de quão bem está o algoritmo de hash das chaves.

Aqui está o teste para mostrar que a versão simultânea provavelmente funciona. :) (Teste sob fogo seria o caminho real).

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



    }

}

Este é o último post. O primeiro post foi excluído porque era um cache LFU, não um LRU.

Eu pensei que daria outra chance. Eu estava tentando tentar a versão mais simples de um cache LRU usando o JDK padrão sem muita implementação.

Aqui está o que eu vim com. Minha primeira tentativa foi um desastre quando implementei uma LFU em vez de e LRU, e então adicionei o FIFO e o suporte à LRU ... e então percebi que estava se tornando um monstro. Então comecei a conversar com meu amigo John, que estava pouco interessado, e depois descrevi detalhadamente como implementei um LFU, LRU e FIFO e como você poderia alterná-lo com um simples argumento ENUM, e então percebi que tudo o que realmente queria era uma LRU simples. Portanto, ignore a postagem anterior e informe-me se você deseja ver um cache LRU / LFU / FIFO que pode ser alternado por meio de uma enumeração ... não? Ok .. aqui vai ele.

A LRU mais simples possível usando apenas o JDK. Eu implementei uma versão simultânea e uma versão não simultânea.

Eu criei uma interface comum (é minimalismo, provavelmente faltando alguns recursos que você gostaria, mas funciona para meus casos de uso, mas deixe que, se quiser ver o recurso XYZ, avise-me ... vivo para escrever código). .

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

Você pode se perguntar o que é o getSilent . Eu uso isso para testar. O getSilent não altera a pontuação LRU de um item.

Primeiro o não concorrente ....

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

A fila.removeFirstOccurrence é uma operação potencialmente cara se você tiver um cache grande. Pode-se usar o LinkedList como exemplo e adicionar um mapa de hash de pesquisa inversa de elemento para nó para tornar as operações de remoção MUITO MAIS RÁPIDAS e mais consistentes. Comecei também, mas depois percebi que não precisava disso. Mas talvez...

Quando put é chamado, a chave é adicionada à fila. Quando get é chamado, a chave é removida e adicionada novamente à parte superior da fila.

Se seu cache é pequeno e a construção de um item é cara, esse deve ser um bom cache. Se seu cache for realmente grande, a pesquisa linear poderá ser um gargalo, especialmente se você não tiver áreas quentes de cache. Quanto mais intensos os pontos de acesso, mais rápida é a pesquisa linear, pois os itens quentes estão sempre no topo da pesquisa linear. Enfim ... o que é necessário para que isso aconteça mais rápido é escrever outro LinkedList que tenha uma operação de remoção que possua elemento reverso na pesquisa de nó para remover e remover seria tão rápido quanto remover uma chave de um mapa de hash.

Se você tiver um cache com menos de 1.000 itens, isso deve funcionar bem.

Aqui está um teste simples para mostrar suas operações em ação.

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

    }
}

O último cache LRU foi de thread único e não o embrulhe em nada sincronizado ....

Aqui está uma facada em uma versão simultânea.

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

As principais diferenças são o uso do ConcurrentHashMap em vez do HashMap e o uso do bloqueio (eu poderia ter me safado do sincronizado, mas ...).

Não testei sob fogo, mas parece um cache LRU simples que pode funcionar em 80% dos casos de uso em que você precisa de um mapa LRU simples.

Congratulo-me com o feedback, exceto o por que você não usa a biblioteca a, b ou c. A razão pela qual nem sempre uso uma biblioteca é porque nem sempre quero que todos os arquivos de guerra tenham 80 MB e escrevo bibliotecas, de modo a tornar as bibliotecas plugáveis ​​com uma solução boa o suficiente e alguém pode conectar -em outro provedor de cache, se quiserem. :) Eu nunca sei quando alguém pode precisar do Guava ou ehcache ou de outra coisa que não queira incluí-los, mas se eu tornar o cache plugável, não os excluirei também.

A redução de dependências tem sua própria recompensa. Gosto de receber algum feedback sobre como tornar isso ainda mais simples ou mais rápido, ou ambos.

Além disso, se alguém souber de um pronto para ir ....

Ok .. eu sei o que você está pensando ... Por que ele simplesmente não usa a entrada removeEldest do LinkedHashMap, e eu deveria mas ... mas ... mas .. Isso seria um FIFO, não um LRU e nós estávamos tentando implementar uma LRU.

    Map<KEY, VALUE> map = new LinkedHashMap<KEY, VALUE> () {

        @Override
        protected boolean removeEldestEntry ( Map.Entry<KEY, VALUE> eldest ) {
            return this.size () > limit;
        }
    };

Este teste falha no código acima ...

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

Então, aqui está um cache FIFO rápido e sujo usando 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 ();
    }
}

FIFOs são rápidos. Sem procurar por aí. Você poderia fazer frente a um FIFO na frente de uma LRU e isso lidaria muito bem com a maioria das entradas quentes. Uma LRU melhor precisará desse elemento reverso para o recurso Nó.

Enfim ... agora que escrevi um código, deixe-me ver as outras respostas e ver o que perdi ... na primeira vez que as digitalizei.

RickHigh
fonte
9

LinkedHashMapé O (1), mas requer sincronização. Não há necessidade de reinventar a roda lá.

2 opções para aumentar a simultaneidade:

1. Crie múltiplas LinkedHashMap, e haxixe para eles: exemplo: LinkedHashMap[4], index 0, 1, 2, 3. Na tecla faça key%4 (ou binary ORligue [key, 3]) para escolher o mapa a ser colocado / obtido / removido.

2. Você pode fazer um 'quase' LRU estendendo ConcurrentHashMape tendo um mapa de hash vinculado como estrutura em cada uma das regiões dentro dele. O bloqueio ocorreria mais granularmente do que LinkedHashMapaquele sincronizado. Em um putou putIfAbsentapenas um bloqueio na cabeça e no final da lista é necessário (por região). Ao remover ou obter, toda a região precisa estar bloqueada. Estou curioso para saber que listas atômicas de algum tipo podem ajudar aqui - provavelmente para o chefe da lista. Talvez por mais.

A estrutura não manteria o pedido total, mas apenas o pedido por região. Contanto que o número de entradas seja muito maior que o número de regiões, isso é bom o suficiente para a maioria dos caches. Cada região terá que ter sua própria contagem de entradas, isso seria usado em vez da contagem global para o gatilho de despejo. O número padrão de regiões em a ConcurrentHashMapé 16, o que é suficiente para a maioria dos servidores atualmente.

  1. seria mais fácil escrever e mais rápido com simultaneidade moderada.

  2. seria mais difícil de escrever, mas dimensionar muito melhor com simultaneidade muito alta. Seria mais lento para o acesso normal (assim como ConcurrentHashMapé mais lento do que HashMaponde não há simultaneidade)

jiaweizhang
fonte
8

Existem duas implementações de código aberto.

O Apache Solr possui ConcurrentLRUCache: https://lucene.apache.org/solr/3_6_1/org/apache/solr/util/ConcurrentLRUCache.html

Há um projeto de código aberto para um ConcurrentLinkedHashMap: http://code.google.com/p/concurrentlinkedhashmap/

Ron
fonte
2
A solução de Solr não é realmente LRU, mas ConcurrentLinkedHashMapé interessante. Ele alega ter sido MapMakerretirado da goiaba, mas eu não o localizei nos documentos. Alguma idéia do que está acontecendo com esse esforço?
Hank Gay
3
Uma versão simplificada foi integrada, mas os testes ainda não foram concluídos, portanto ainda não são públicos. Eu tive muitos problemas ao fazer uma integração mais profunda, mas espero concluí-la, pois existem algumas propriedades algorítmicas agradáveis. A capacidade de ouvir um despejo (capacidade, expiração, GC) foi adicionada e baseia-se na abordagem do CLHM (fila do ouvinte). Também gostaria de contribuir com a idéia de "valores ponderados", pois isso é útil ao armazenar em cache coleções. Infelizmente, devido a outros compromissos, fui muito atarefado para dedicar o tempo que a Goiaba merece (e que prometi a Kevin / Charles).
Ben Manes
3
Atualização: a integração foi concluída e pública na Guava r08. Isso através da configuração #maximumSize ().
Ben Manes
7

Eu consideraria o uso de java.util.concurrent.PriorityBlockingQueue , com prioridade determinada por um contador "numberOfUses" em cada elemento. Eu teria muito, muito cuidado para corrigir toda a minha sincronização, pois o contador "numberOfUses" implica que o elemento não pode ser imutável.

O objeto de elemento seria um wrapper para os objetos no cache:

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

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

    ... etc.
}
Steve McLeod
fonte
você não quer dizer deve ser imutável?
shsteimer 21/10/08
2
Observe que se você tentar executar a versão priorityblockingqueue mencionada por steve mcleod, imutável, porque modificar o elemento enquanto estiver na fila não terá efeito, será necessário remover o elemento e adicioná-lo novamente para priorize-o novamente.
james
James abaixo aponta um erro que cometi. O que ofereço como evidência de quão difícil é escrever caches confiáveis ​​e resistentes.
Steve McLeod
6

Espero que isto ajude .

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);
}
}
murasing
fonte
1
Belo exemplo! Você poderia comentar por que precisa definir a capacidade maxSize * 4/3?
Akvel 28/10
1
O @Akvel é chamado de capacidade inicial, pode ser qualquer valor [inteiro], enquanto 0,75f é o fator de carga padrão, espero que este link ajude: ashishsharma.me/2011/09/custom-lru-cache-java.html
murasing
5

O cache do LRU pode ser implementado usando um ConcurrentLinkedQueue e um ConcurrentHashMap, que também podem ser usados ​​no cenário de multithreading. O cabeçalho da fila é o elemento que está na fila há mais tempo. A cauda da fila é o elemento que está na fila há menos tempo. Quando um elemento existe no mapa, podemos removê-lo do LinkedQueue e inseri-lo no final.

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

}
sanjanab
fonte
Isso não é seguro para threads. Por exemplo, você pode facilmente exceder o tamanho máximo de LRU chamando simultaneamente put.
Dpeacock
Por favor corrija isso. Antes de tudo, ele não compila na linha map.containsKey (key). Em segundo lugar, em get (), você deve verificar se a chave foi realmente removida, caso contrário, o mapa e a fila ficam fora de sincronia e "queue.size ()> = size" se torna sempre verdadeiro. Vou postar minha versão corrigida, pois gostei da sua ideia de usar essas duas coleções.
Aleksander Lech
3

Aqui está minha implementação para LRU. Eu usei PriorityQueue, que basicamente funciona como FIFO e não é seguro para threads. O Comparador usado com base na criação do tempo da página e com base na execução das ordens das páginas pelo tempo usado menos recentemente.

Páginas para consideração: 2, 1, 0, 2, 8, 2, 4

A página adicionada ao cache é: 2 A
página adicionada ao cache é: 1 A
página adicionada ao cache é: 0 A
página: 2 já existe no cache. O último horário de acesso atualizado foi atualizado com
falha de página, PÁGINA: 1, substituída por PÁGINA: 8 A
página adicionada ao cache é: 8
Página: 2 já existe no cache. Última hora de acesso atualizada atualizada
Falha na página, PÁGINA: 0, Substituída por PÁGINA: 4 A
página adicionada ao cache é: 4

RESULTADO

Páginas do LRUCache
------------- Nome da Página
: 8, PageCreationTime: 1365957019974 Nome da Página
: 2, PageCreationTime: 1365957020074 Nome da Página
: 4, PageCreationTime: 1365957020174

entre com o código aqui

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;
    }
}
Deepak Singhvi
fonte
2

Aqui está minha implementação simultânea de cache LRU de melhor desempenho testada sem nenhum bloco sincronizado:

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

}

Zoltan Boda
fonte
1
@zoltan boda .... você não lidou com uma situação .. e se o mesmo objeto for usado várias vezes? neste caso, não deve adicionar várias entradas para o mesmo objeto ... em vez sua chave deve ser
5
Aviso: Este não é um cache LRU. Em um cache LRU, você joga fora os itens acessados ​​menos recentemente. Este joga fora os itens menos escritos recentemente. Também é uma varredura linear para executar a operação queue.remove (key).
Dave L.
ConcurrentLinkedQueue # size () também não é uma operação de tempo constante.
NateS 31/05/12
3
Seu método put não parece seguro - ele tem algumas instruções de verificação e ação que serão interrompidas com vários threads.
Assilias
2

Esse é o cache LRU que eu uso, que encapsula um LinkedHashMap e lida com a simultaneidade com um bloqueio de sincronização simples que protege os pontos interessantes. Ele "toca" os elementos à medida que são usados, para que se tornem o elemento "mais recente" novamente, de modo que na verdade é LRU. Eu também tinha o requisito de que meus elementos tivessem uma vida útil mínima, que você também pode considerar como o "tempo ocioso máximo" permitido, então você estará pronto para despejo.

No entanto, concordo com a conclusão de Hank e aceitei a resposta - se eu estivesse começando isso de novo hoje, verificaria o Goiaba 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();
        }
    }

}
broc.seib
fonte
2

Bem, para um cache, você geralmente procurará alguns dados por meio de um objeto proxy (uma URL, String ...), de modo que, na interface, você desejará um mapa. mas para começar, você quer uma fila como a estrutura. Internamente, eu manteria duas estruturas de dados, uma Fila prioritária e um HashMap. aqui está uma implementação que deve ser capaz de fazer tudo em O (1) tempo.

Aqui está uma aula que eu iniciei bem rápido:

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

Aqui está como isso funciona. As chaves são armazenadas em uma lista vinculada com as chaves mais antigas na frente da lista (novas chaves retornam), então, quando você precisa 'ejetar' algo, basta colocá-lo na frente da fila e usar a tecla para remova o valor do mapa. Quando um item é referenciado, você pega o ValueHolder no mapa e, em seguida, usa a variável queuelocation para remover a chave da sua localização atual na fila e, em seguida, coloca-a na parte de trás da fila (agora é a mais usada recentemente). Adicionar coisas é praticamente o mesmo.

Tenho certeza de que há muitos erros aqui e não implementei nenhuma sincronização. mas essa classe fornecerá O (1) adicionando ao cache, O (1) remoção de itens antigos e O (1) recuperação de itens de cache. Mesmo uma sincronização trivial (apenas sincronize todos os métodos públicos) ainda teria pouca contenção de bloqueio devido ao tempo de execução. Se alguém tiver algum truque inteligente de sincronização, eu ficaria muito interessado. Além disso, tenho certeza de que há algumas otimizações adicionais que você pode implementar usando a variável maxsize em relação ao mapa.

Lucas
fonte
Obrigado pelo nível de detalhe, mas onde isso proporciona uma vitória sobre a implementação LinkedHashMap+ Collections.synchronizedMap()?
Hank Gay
Desempenho, não sei ao certo, mas não acho que o LinkedHashMap tenha inserção O (1) (provavelmente O (log (n))); na verdade, você pode adicionar alguns métodos para concluir a interface do mapa na minha implementação e use Collections.synchronizedMap para adicionar simultaneidade.
luke
Na classe LinkedList acima no método add, há um código no bloco else, por exemplo, if (tail.prev == null) {tail.prev = head; cabeça.próximo = cauda; Quando esse código será executado? Fiz algumas corridas a seco e acho que isso nunca será executado e deve ser removido.
Dipesh

1

Dê uma olhada no ConcurrentSkipListMap . Ele deve fornecer um tempo de log (n) para testar e remover um elemento, se ele já estiver contido no cache, e tempo constante para adicioná-lo novamente.

Você precisaria apenas de um contador etc e um elemento wrapper para forçar a ordem da ordem LRU e garantir que itens recentes sejam descartados quando o cache estiver cheio.


Iria ConcurrentSkipListMapfornecer algum benefício facilidade de implementação ao longo ConcurrentHashMap, ou é simplesmente um caso de evitar casos patológicos?
Hank Gay

Isso tornaria as coisas mais simples, pois o ConcurrentSkipListMap ordena os elementos, o que permitiria gerenciar em que ordem as coisas foram usadas. O ConcurrentHashMap não faz isso; portanto, você basicamente precisa iterar todo o conteúdo do cache para atualizar o último elemento. contador usado 'ou o que quer #
madlep 21/10/08

Portanto, com a ConcurrentSkipListMapimplementação, eu criaria uma nova implementação da Mapinterface que delega ConcurrentSkipListMape executa algum tipo de quebra automática para que os tipos de chave arbitrários sejam quebrados em um tipo que seja facilmente classificado com base no último acesso?
Hank Gay

1

Aqui está a minha curta implementação, por favor, critique ou melhore!

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
Este não é o cache LRU, apenas o cache FIFO.
Lslab 24/03

1

Aqui está minha própria implementação para esse problema

O simplelrucache fornece armazenamento em cache LRU seguro, muito simples e não distribuído, com suporte a TTL. Ele fornece duas implementações:

  • Simultâneo com base em ConcurrentLinkedHashMap
  • Sincronizado com base no LinkedHashMap

Você pode encontrá-lo aqui: http://code.google.com/p/simplelrucache/


1

A melhor maneira de conseguir isso é usar um LinkedHashMap que mantenha a ordem de inserção dos elementos. A seguir está um código de exemplo:

public class Solution {

Map<Integer,Integer> cache;
int capacity;
public Solution(int capacity) {
    this.cache = new LinkedHashMap<Integer,Integer>(capacity); 
    this.capacity = capacity;

}

// This function returns false if key is not 
// present in cache. Else it moves the key to 
// front by first removing it and then adding 
// it, and returns true. 

public int get(int key) {
if (!cache.containsKey(key)) 
        return -1; 
    int value = cache.get(key);
    cache.remove(key); 
    cache.put(key,value); 
    return cache.get(key); 

}

public void set(int key, int value) {

    // If already present, then  
    // remove it first we are going to add later 
       if(cache.containsKey(key)){
        cache.remove(key);
    }
     // If cache size is full, remove the least 
    // recently used. 
    else if (cache.size() == capacity) { 
        Iterator<Integer> iterator = cache.keySet().iterator();
        cache.remove(iterator.next()); 
    }
        cache.put(key,value);
}

}

Dhirendra Gautam
Isso é um FIFO. Ele pediu uma LRU.
RickHigh
Falha neste teste ... cache.get (2); cache.get (3); cache.put (6, 6); cache.put (7, 7); ok | = cache.size () == 4 || die ("tamanho" + cache.size ()); ok | = cache.getSilent (2) == 2 || morrer (); ok | = cache.getSilent (3) == 3 || morrer (); ok | = cache.getSilent (4) == nulo || morrer (); ok | = cache.getSilent (5) == nulo || morrer ();
RickHigh
0

Estou procurando um cache LRU melhor usando código Java. É possível compartilhar seu código de cache Java LRU usando LinkedHashMape Collections#synchronizedMap? Atualmente, estou usando LRUMap implements Mape o código funciona bem, mas estou fazendo ArrayIndexOutofBoundExceptiono teste de carga usando 500 usuários no método abaixo. O método move o objeto recente para a frente da fila.

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)e put(Object key, Object value)método chama o moveToFrontmétodo acima .

Raj Pandian
fonte
0

Queria adicionar um comentário à resposta dada por Hank, mas de alguma forma eu não sou capaz - por favor, trate-a como comentário

O LinkedHashMap mantém a ordem de acesso também com base no parâmetro passado em seu construtor. Ele mantém uma lista duplamente alinhada para manter a ordem (consulte LinkedHashMap.Entry)

@Pacerier, é correto que o LinkedHashMap mantenha a mesma ordem durante a iteração se o elemento for adicionado novamente, mas isso ocorre apenas no modo de ordem de inserção.

foi o que encontrei nos documentos java do objeto 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);
        }
    }

esse método cuida de mover o elemento acessado recentemente para o final da lista. Portanto, o LinkedHashMap é a melhor estrutura de dados para a implementação do LRUCache.

Abhishek Gayakwad
fonte
0

Outro pensamento e até uma implementação simples usando a coleção de Java LinkedHashMap.

O método LinkedHashMap forneceu removeEldestEntry e que pode ser substituído da maneira mencionada no exemplo. Por padrão, a implementação dessa estrutura de coleção é falsa. Se seu verdadeiro e tamanho dessa estrutura exceder a capacidade inicial, os elementos mais velhos ou mais antigos serão removidos.

Podemos ter um pageno e o conteúdo da página no meu caso pageno é um número inteiro e pagecontent eu mantive a string de valores de número de página.

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

     }

}

O resultado da execução do código acima é o seguinte:

 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 
Deepak Singhvi
fonte
Isso é um FIFO. Ele pediu uma LRU.
RickHigh
Falha neste teste ... cache.get (2); cache.get (3); cache.put (6, 6); cache.put (7, 7); ok | = cache.size () == 4 || die ("tamanho" + cache.size ()); ok | = cache.getSilent (2) == 2 || morrer (); ok | = cache.getSilent (3) == 3 || morrer (); ok | = cache.getSilent (4) == nulo || morrer (); ok | = cache.getSilent (5) == nulo || morrer ();
RickHigh
0

Seguindo o conceito @sanjanab (mas após as correções), fiz minha versão do LRUCache fornecendo também o Consumidor que permite fazer algo com os itens removidos, se necessário.

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);
    }
}
Aleksander Lech
fonte