cache java caching data-structures lru

cache - ¿Cómo implementaría un caché LRU en Java?



lru cache python (20)

No digas EHCache ni OSCache, etc. Asume a los efectos de esta pregunta que quiero implementar la mía utilizando solo el SDK (aprender haciendo). Dado que la memoria caché se usará en un entorno multiproceso, ¿qué estructuras de datos usarías? Ya he implementado uno usando LinkedHashMap y Collections#synchronizedMap , pero tengo curiosidad por si alguna de las nuevas colecciones concurrentes sería mejor candidata.

ACTUALIZACIÓN: Estaba leyendo la última versión de Yegge cuando encontré esta pepita:

Si necesita acceso de tiempo constante y desea mantener el orden de inserción, no puede hacer mejor que un LinkedHashMap, una estructura de datos realmente maravillosa. La única forma en que podría ser más maravilloso es si hubiera una versión concurrente. Pero Ay.

Estaba pensando casi exactamente lo mismo antes de ir con la implementación LinkedHashMap + Collections#synchronizedMap que mencioné anteriormente. Es bueno saber que no había pasado por alto algo.

En base a las respuestas hasta ahora, parece que mi mejor opción para un LRU altamente concurrente sería extender ConcurrentHashMap utilizando la misma lógica que usa LinkedHashMap .


Aquí está mi implementación de caché de LRU concurrente probada de mejor rendimiento sin ningún bloque 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); }

}


Aquí está mi implementación para LRU. He usado PriorityQueue, que básicamente funciona como FIFO y no es seguro. Comparador usado basado en la creación de tiempo de la página y basado en el orden de las páginas para el tiempo utilizado menos recientemente.

Páginas para consideración: 2, 1, 0, 2, 8, 2, 4

La página agregada al caché es: 2
La página agregada al caché es: 1
La página agregada al caché es: 0
Página: 2 ya existe en caché. Último tiempo de acceso actualizado
Error de página, PÁGINA: 1, reemplazado con PÁGINA: 8
La página agregada al caché es: 8
Página: 2 ya existe en caché. Último tiempo de acceso actualizado
Error de página, PÁGINA: 0, reemplazado con PÁGINA: 4
La página agregada al caché es: 4

SALIDA

LRUCache Pages
-------------
Nombre de página: 8, PageCreationTime: 1365957019974
Nombre de la página: 2, PageCreationTime: 1365957020074
PageName: 4, PageCreationTime: 1365957020174

Ingresa el código aquí

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


Consideraría usar java.util.concurrent.PriorityBlockingQueue , con la prioridad determinada por un contador "numberOfUses" en cada elemento. Sería muy, muy cuidadoso para corregir toda mi sincronización, ya que el contador "numberOfUses" implica que el elemento no puede ser inmutable.

El objeto element sería un contenedor para los objetos en la memoria caché:

class CacheElement { private final Object obj; private int numberOfUsers = 0; CacheElement(Object obj) { this.obj = obj; } ... etc. }


Espero que esto ayude .

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


Esta es la segunda ronda.

La primera ronda fue lo que se me ocurrió y luego volví a leer los comentarios con el dominio un poco más arraigado en mi cabeza.

Así que aquí está la versión más simple con una prueba de unidad que muestra que funciona en base a algunas otras versiones.

Primero la versión no concurrente:

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

La bandera verdadera rastreará el acceso de los puts y puts. Ver JavaDocs. El removeEdelstEntry sin el indicador verdadero para el constructor simplemente implementaría un caché FIFO (ver notas debajo en FIFO y eliminar EldestEntry).

Aquí está la prueba que prueba que funciona como un caché 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 (); }

Ahora para la versión concurrente ...

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

Puede ver por qué cubro la versión no concurrente primero. Lo anterior intenta crear algunas rayas para reducir la contención de bloqueo. Así que nos hash la clave y luego busca ese hash para encontrar el caché real. Esto hace que el tamaño límite sea más una sugestión / suposición aproximada dentro de una cantidad justa de error dependiendo de qué tan bien se disemine el algoritmo hash de sus claves.

Aquí está la prueba para mostrar que la versión simultánea probablemente funcione. :) (La prueba bajo fuego sería la manera 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 (); } }

Esta es la última publicación ... La primera publicación la eliminé porque era una LFU, no una caché LRU.

Pensé en darle otra oportunidad. Trataba de encontrar la versión más simple de un caché LRU usando el estándar JDK sin demasiada implementación.

Esto es lo que se me ocurrió. Mi primer intento fue un desastre ya que implementé un LFU en lugar de LRU, y luego agregué FIFO y el soporte de LRU ... y luego me di cuenta de que se estaba convirtiendo en un monstruo. Luego comencé a hablar con mi amigo John, que apenas estaba interesado, y luego describí en profundidad cómo implementé un LFU, LRU y FIFO y cómo se podía cambiar con un simple ENUM arg, y luego me di cuenta de que todo lo que realmente quería era un LRU simple Por lo tanto, ignóreme la publicación anterior y avíseme si desea ver una caché LRU / LFU / FIFO que se pueda cambiar a través de una enumeración ... ¿no? Ok ... aquí va.

El LRU más simple posible usando solo el JDK. Implementé una versión simultánea y una versión no concurrente.

Creé una interfaz común (es minimalismo, es probable que te falten algunas funciones que te gustaría, pero funciona para mis casos de uso, pero deja que si quieres ver la función XYZ, házmelo saber ... vivo para escribir el 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 (); }

Puede preguntarse qué es getSilent . Yo uso esto para probar getSilent no cambia la puntuación LRU de un elemento.

Primero el no concurrente ...

import java.util.Deque; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; public class LruCacheNormal<KEY, VALUE> implements LruCache<KEY,VALUE> { Map<KEY, VALUE> map = new HashMap<> (); Deque<KEY> queue = new LinkedList<> (); final int limit; public LruCacheNormal ( int limit ) { this.limit = limit; } public void put ( KEY key, VALUE value ) { VALUE oldValue = map.put ( key, value ); /*If there was already an object under this key, then remove it before adding to queue Frequently used keys will be at the top so the search could be fast. */ if ( oldValue != null ) { queue.removeFirstOccurrence ( key ); } queue.addFirst ( key ); if ( map.size () > limit ) { final KEY removedKey = queue.removeLast (); map.remove ( removedKey ); } } public VALUE get ( KEY key ) { /* Frequently used keys will be at the top so the search could be fast.*/ queue.removeFirstOccurrence ( key ); queue.addFirst ( key ); return map.get ( key ); } public VALUE getSilent ( KEY key ) { return map.get ( key ); } public void remove ( KEY key ) { /* Frequently used keys will be at the top so the search could be fast.*/ queue.removeFirstOccurrence ( key ); map.remove ( key ); } public int size () { return map.size (); } public String toString() { return map.toString (); } }

Queue.removeFirstOccurrence es una operación potencialmente costosa si tiene un caché grande. Uno podría tomar LinkedList como ejemplo y agregar un mapa hash de búsqueda inversa de un elemento a otro para hacer que las operaciones de eliminación sean MUCHO MÁS RÁPIDAS y más consistentes. Empecé también, pero luego me di cuenta de que no lo necesitaba. Pero tal vez...

Cuando se invoca put , la clave se agrega a la cola. Cuando se llama a get , la clave se elimina y se vuelve a agregar a la parte superior de la cola.

Si su caché es pequeña y la construcción de un artículo es costosa, entonces esta debería ser una buena caché. Si su caché es realmente grande, entonces la búsqueda lineal podría ser un cuello de botella, especialmente si no tiene áreas calientes de caché. Cuanto más intensos sean los puntos críticos, más rápida será la búsqueda lineal, ya que los elementos más importantes siempre se encuentran en la parte superior de la búsqueda lineal. De todos modos ... lo que se necesita para que esto sea más rápido es escribir otra LinkedList que tenga una operación de eliminación que tenga un elemento inverso para la búsqueda de nodos para eliminar, luego eliminar sería tan rápido como quitar una clave de un mapa hash.

Si tiene un caché por debajo de 1,000 elementos, esto debería funcionar bien.

Aquí hay una prueba simple para mostrar sus operaciones en acción.

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

El último caché de LRU tenía un único subproceso, y no lo envuelva en un archivo sincronizado ...

Aquí hay una puñalada en una versión concurrente.

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

Las principales diferencias son el uso de ConcurrentHashMap en lugar de HashMap, y el uso del bloqueo (podría haber salido con sincronización, pero ...).

No lo he probado bajo fuego, pero parece que un simple caché LRU podría funcionar en el 80% de los casos de uso donde se necesita un mapa LRU simple.

Agradezco sus comentarios, excepto por qué no usa la biblioteca a, b, o c. La razón por la que no siempre utilizo una biblioteca es porque no siempre quiero que cada archivo war sea de 80MB, y escribo bibliotecas, por lo que tiendo a hacer que las librerías se puedan conectar con una solución lo suficientemente buena en su lugar y alguien puede conectarse -en otro proveedor de caché si así lo desean. :) Nunca sé cuándo alguien podría necesitar Guava o ehcache u otra cosa, no quiero incluirlos, pero si hago que el almacenamiento en caché sea conectable, tampoco los excluiré.

La reducción de dependencias tiene su propia recompensa. Me encanta recibir comentarios sobre cómo hacer que esto sea aún más simple o más rápido, o ambos.

Además, si alguien sabe de un listo para ir ...

De acuerdo ... Sé lo que estás pensando ... ¿Por qué no usa simplemente removeEldest entry de LinkedHashMap, y bueno debería pero ... pero ... pero ... Eso sería un FIFO no un LRU y estábamos tratando de implementar un LRU.

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

Esta prueba falla para el código anterior ...

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

Así que aquí hay un caché FIFO rápido y sucio 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 son rápidos. Sin buscar alrededor. Podrías hacer frente a una FIFO frente a una LRU y eso manejaría la mayoría de las entradas calientes bastante bien. Un LRU mejor va a necesitar ese elemento inverso a la función Nodo.

De todos modos ... ahora que escribí un código, déjame ir a través de las otras respuestas y ver lo que me perdí ... la primera vez que las escaneé.



LRU Cache se puede implementar utilizando un ConcurrentLinkedQueue y un ConcurrentHashMap que también se puede usar en escenarios de subprocesamiento múltiple. El jefe de la cola es el elemento que ha estado en la cola más tiempo. La cola de la cola es el elemento que ha estado en la cola el tiempo más corto. Cuando existe un elemento en el Mapa, podemos eliminarlo de LinkedQueue e insertarlo en la cola.

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


Me gustan muchas de estas sugerencias, pero por ahora creo que me quedaré con LinkedHashMap + Collections.synchronizedMap . Si reviso esto en el futuro, probablemente trabajaré en la extensión de ConcurrentHashMap de la misma manera que LinkedHashMap amplía HashMap .

ACTUALIZAR:

A pedido, esta es la esencia de mi implementación actual.

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



Si estuviera haciendo esto de nuevo desde cero hoy, usaría el CacheBuilder de Guava.


LinkedHashMap es O (1), pero requiere sincronización. No hay necesidad de reinventar la rueda allí.

2 opciones para aumentar la concurrencia:

1. Cree Multiple LinkedHashMap y hash en ellos: ejemplo: LinkedHashMap[4], index 0, 1, 2, 3 . En la tecla, haga key%4 (u binary OR en [key, 3] ) para elegir qué mapa hacer un put / get / remove.

2. Podrías hacer un LRU ''casi'' al extender ConcurrentHashMap , y tener una estructura similar a un mapa hash enlazado en cada una de las regiones dentro de él. El bloqueo ocurriría más granularmente que un LinkedHashMap que está sincronizado. En una put o putIfAbsent solo se putIfAbsent un bloqueo en la cabeza y la cola de la lista (por región). En una eliminación u obtener toda la región debe estar bloqueado. Tengo curiosidad por saber si las listas vinculadas de Atomic de algún tipo podrían ser útiles aquí, probablemente también para el líder de la lista. Quizás para más.

La estructura no mantendría el orden total, sino solo el orden por región. Siempre que el número de entradas sea mucho mayor que el número de regiones, esto es lo suficientemente bueno para la mayoría de las memorias caché. Cada región tendrá que tener su propio conteo de entradas, esto se usaría en lugar del conteo global para el desencadenante de desalojo. El número predeterminado de regiones en un ConcurrentHashMap es 16, que es suficiente para la mayoría de los servidores de hoy.

  1. sería más fácil de escribir y más rápido en concurrencia moderada.

  2. sería más difícil de escribir, pero escalaría mucho mejor a una concurrencia muy alta. Sería más lento para el acceso normal (al igual que ConcurrentHashMap es más lento que HashMap donde no hay concurrencia)


Another thought and even a simple implementation using LinkedHashMap collection of Java.

LinkedHashMap provided method removeEldestEntry and which can be overridden in the way mentioned in example. By default implementation of this collection structure is false. If its true and size of this structure goes beyond the initial capacity than eldest or older elements will be removed.

We can have a pageno and page content in my case pageno is integer and pagecontent i have kept page number values string.

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

Result of above code execution is as follows:

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


Have a look at ConcurrentSkipListMap . It should give you log(n) time for testing and removing an element if it is already contained in the cache, and constant time for re-adding it.

You''d just need some counter etc and wrapper element to force ordering of the LRU order and ensure recent stuff is discarded when the cache is full.


Here is my short implementation, please criticize or improve it!

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


Here''s my own implementation to this problem

simplelrucache provides threadsafe, very simple, non-distributed LRU caching with TTL support. It provides two implementations:

  • Concurrent based on ConcurrentLinkedHashMap
  • Synchronized based on LinkedHashMap

You can find it here: http://code.google.com/p/simplelrucache/


I''m looking for a better LRU cache using Java code. Is it possible for you to share your Java LRU cache code using LinkedHashMap and Collections#synchronizedMap ? Currently I''m using LRUMap implements Map and the code works fine, but I''m getting ArrayIndexOutofBoundException on load testing using 500 users on the below method. The method moves the recent object to front of the queue.

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) and put(Object key, Object value) method calls the above moveToFront method.


This is the LRU cache I use, which encapsulates a LinkedHashMap and handles concurrency with a simple synchronize lock guarding the juicy spots. It "touches" elements as they are used so that they become the "freshest" element again, so that it is actually LRU. I also had the requirement of my elements having a minimum lifespan, which you can also think of as "maximum idle time" permitted, then you''re up for eviction.

However, I agree with Hank''s conclusion and accepted answer -- if I were starting this again today, I''d check out Guava''s 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(); } } }


Wanted to add comment to the answer given by Hank but some how I am not able to - please treat it as comment

LinkedHashMap maintains access order as well based on parameter passed in its constructor It keeps doubly lined list to maintain order (See LinkedHashMap.Entry)

@Pacerier it is correct that LinkedHashMap keeps same order while iteration if element is added again but that is only in case of insertion order mode.

this is what I found in java docs of LinkedHashMap.Entry object

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

this method takes care of moving recently accessed element to end of the list. So all in all LinkedHashMap is best data structure for implementing LRUCache.


Well for a cache you will generally be looking up some piece of data via a proxy object, (a URL, String....) so interface-wise you are going to want a map. but to kick things out you want a queue like structure. Internally I would maintain two data structures, a Priority-Queue and a HashMap. heres an implementation that should be able to do everything in O(1) time.

Here''s a class I whipped up pretty quick:

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

Here''s how it works. Keys are stored in a linked list with the oldest keys in the front of the list (new keys go to the back) so when you need to ''eject'' something you just pop it off the front of the queue and then use the key to remove the value from the map. When an item gets referenced you grab the ValueHolder from the map and then use the queuelocation variable to remove the key from its current location in the queue and then put it at the back of the queue (its now the most recently used). Adding things is pretty much the same.

I''m sure theres a ton of errors here and I haven''t implemented any synchronization. but this class will provide O(1) adding to the cache, O(1) removal of old items, and O(1) retrieval of cache items. Even a trivial synchronization (just synchronize every public method) would still have little lock contention due to the run time. If anyone has any clever synchronization tricks I would be very interested. Also, I''m sure there are some additional optimizations that you could implement using the maxsize variable with respect to the map.


Android ofrece una implementación de un LRU Cache . El code es limpio y directo.