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#synchronizedMap
mencionada 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 LinkedHashMap
usadas.
fonte
O(1)
versão exigida: stackoverflow.com/questions/23772102/…Respostas:
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ãoConcurrentHashMap
da mesma maneira que elaLinkedHashMap
se estendeHashMap
.ATUALIZAR:
Por solicitação, aqui está a essência da minha implementação atual.
fonte
LinkedHashMap
endossam explicitamente esse método para criar uma implementação de LRU.Se eu estivesse fazendo isso novamente do zero hoje, usaria goiaba
CacheBuilder
.fonte
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:
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:
Agora, a versão simultânea ...
pacote org.boon.cache;
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).
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). .
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 ....
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.
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.
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.
Este teste falha no código acima ...
Então, aqui está um cache FIFO rápido e sujo usando removeEldestEntry.
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.
fonte
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çakey%4
(oubinary OR
ligue[key, 3]
) para escolher o mapa a ser colocado / obtido / removido.2. Você pode fazer um 'quase' LRU estendendo
ConcurrentHashMap
e tendo um mapa de hash vinculado como estrutura em cada uma das regiões dentro dele. O bloqueio ocorreria mais granularmente do queLinkedHashMap
aquele sincronizado. Em umput
ouputIfAbsent
apenas 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.seria mais fácil escrever e mais rápido com simultaneidade moderada.
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 queHashMap
onde não há simultaneidade)fonte
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/
fonte
ConcurrentLinkedHashMap
é interessante. Ele alega ter sidoMapMaker
retirado da goiaba, mas eu não o localizei nos documentos. Alguma idéia do que está acontecendo com esse esforço?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:
fonte
Espero que isto ajude .
fonte
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.
fonte
put
.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
fonte
Aqui está minha implementação simultânea de cache LRU de melhor desempenho testada sem nenhum bloco sincronizado:
}
fonte
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
.fonte
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:
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.
fonte
LinkedHashMap
+Collections.synchronizedMap()
?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.
fonte
ConcurrentSkipListMap
fornecer algum benefício facilidade de implementação ao longoConcurrentHashMap
, ou é simplesmente um caso de evitar casos patológicos?ConcurrentSkipListMap
implementação, eu criaria uma nova implementação daMap
interface que delegaConcurrentSkipListMap
e 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?Aqui está a minha curta implementação, por favor, critique ou melhore!
fonte
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:
Você pode encontrá-lo aqui: http://code.google.com/p/simplelrucache/
fonte
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:
}
Estou procurando um cache LRU melhor usando código Java. É possível compartilhar seu código de cache Java LRU usando
LinkedHashMap
eCollections#synchronizedMap
? Atualmente, estou usandoLRUMap implements Map
e o código funciona bem, mas estou fazendoArrayIndexOutofBoundException
o teste de carga usando 500 usuários no método abaixo. O método move o objeto recente para a frente da fila.get(Object key)
eput(Object key, Object value)
método chama omoveToFront
método acima .fonte
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
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.
fonte
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.
O resultado da execução do código acima é o seguinte:
fonte
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.
fonte
O Android oferece uma implementação de um cache LRU . O código é limpo e direto.
fonte