Dies ist die zweite Runde.
Die erste Runde war das, was ich mir ausgedacht habe, dann habe ich die Kommentare mit der Domain noch einmal etwas tiefer in meinem Kopf verwurzelt gelesen.
Hier ist also die einfachste Version mit einem Komponententest, der zeigt, dass sie auf der Grundlage einiger anderer Versionen funktioniert.
Zuerst die nicht gleichzeitige Version:
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 ();
}
}
Das True-Flag verfolgt den Zugriff auf Gets und Puts. Siehe JavaDocs. Der removeEdelstEntry ohne das true-Flag für den Konstruktor würde lediglich einen FIFO-Cache implementieren (siehe Hinweise unten zu FIFO und removeEldestEntry).
Hier ist der Test, der beweist, dass er als LRU-Cache funktioniert:
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 ();
}
Nun zur gleichzeitigen Version ...
Paket 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 ();
}
}
Sie können sehen, warum ich zuerst die nicht gleichzeitige Version behandele. Die obigen Versuche versuchen, einige Streifen zu erstellen, um Sperrenkonflikte zu reduzieren. Also hasht es den Schlüssel und sucht dann diesen Hash, um den tatsächlichen Cache zu finden. Dies macht die Grenzgröße eher zu einem Vorschlag / einer groben Vermutung innerhalb einer angemessenen Menge von Fehlern, abhängig davon, wie gut Ihr Schlüssel-Hash-Algorithmus verbreitet ist.
Hier ist der Test, um zu zeigen, dass die gleichzeitige Version wahrscheinlich funktioniert. :) (Test unter Beschuss wäre der echte Weg).
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 ();
}
}
Dies ist der letzte Beitrag. Der erste Beitrag, den ich gelöscht habe, war eine LFU und kein LRU-Cache.
Ich dachte, ich würde es noch einmal versuchen. Ich habe versucht, die einfachste Version eines LRU-Caches mit dem Standard-JDK ohne zu viel Implementierung zu entwickeln.
Folgendes habe ich mir ausgedacht. Mein erster Versuch war ein bisschen katastrophal, als ich anstelle von und LRU eine LFU implementierte und dann FIFO und LRU-Unterstützung hinzufügte ... und dann merkte ich, dass es ein Monster wurde. Dann fing ich an, mit meinem Kumpel John zu sprechen, der kaum interessiert war, und dann beschrieb ich ausführlich, wie ich eine LFU, LRU und FIFO implementierte und wie man sie mit einem einfachen ENUM-Argument wechseln konnte, und dann wurde mir klar, dass alles, was ich wirklich wollte war eine einfache LRU. Ignorieren Sie also den früheren Beitrag von mir und lassen Sie mich wissen, ob Sie einen LRU / LFU / FIFO-Cache sehen möchten, der über eine Aufzählung umgeschaltet werden kann ... nein? Ok .. hier geht er.
Die einfachste mögliche LRU, die nur das JDK verwendet. Ich habe sowohl eine gleichzeitige als auch eine nicht gleichzeitige Version implementiert.
Ich habe eine gemeinsame Benutzeroberfläche erstellt (es ist ein Minimalismus, dem wahrscheinlich einige Funktionen fehlen, die Sie möchten, aber er funktioniert für meine Anwendungsfälle. Wenn Sie jedoch die Funktion XYZ sehen möchten, lassen Sie es mich wissen ... Ich lebe, um Code zu schreiben.) .
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 ();
}
Sie fragen sich vielleicht, was getSilent ist. Ich benutze dies zum Testen. getSilent ändert die LRU-Punktzahl eines Elements nicht.
Zuerst die nicht gleichzeitige ....
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 ();
}
}
Die queue.removeFirstOccurrence ist eine möglicherweise teure Operation, wenn Sie einen großen Cache haben. Man könnte LinkedList als Beispiel nehmen und eine Reverse-Lookup-Hash-Map von Element zu Knoten hinzufügen, um Entfernungsoperationen VIEL SCHNELLER und konsistenter zu machen. Ich habe auch angefangen, aber dann wurde mir klar, dass ich es nicht brauche. Aber vielleicht...
Wenn put aufgerufen wird, wird der Schlüssel zur Warteschlange hinzugefügt. Wenn get aufgerufen wird, wird der Schlüssel entfernt und oben in der Warteschlange wieder hinzugefügt.
Wenn Ihr Cache klein ist und das Erstellen eines Elements teuer ist, sollte dies ein guter Cache sein. Wenn Ihr Cache wirklich groß ist, kann die lineare Suche ein Flaschenhals sein, insbesondere wenn Sie keine heißen Cache-Bereiche haben. Je intensiver die Hot Spots sind, desto schneller ist die lineare Suche, da Hot Items immer ganz oben in der linearen Suche stehen. Wie auch immer ... Damit dies schneller geht, muss eine weitere LinkedList geschrieben werden, die über eine Entfernungsoperation verfügt, bei der ein umgekehrtes Element zur Knotensuche zum Entfernen verwendet wird. Das Entfernen ist dann ungefähr so schnell wie das Entfernen eines Schlüssels aus einer Hash-Map.
Wenn Sie einen Cache unter 1.000 Elementen haben, sollte dies gut funktionieren.
Hier ist ein einfacher Test, um seine Operationen in Aktion zu zeigen.
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 ();
}
}
Der letzte LRU-Cache war Single-Threaded, und bitte verpacken Sie ihn nicht in etwas Synchronisiertes.
Hier ist ein Stich in eine gleichzeitige Version.
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 ();
}
}
Die Hauptunterschiede sind die Verwendung der ConcurrentHashMap anstelle von HashMap und die Verwendung der Sperre (ich hätte mit synchronisiert davonkommen können, aber ...).
Ich habe es nicht unter Beschuss getestet, aber es scheint ein einfacher LRU-Cache zu sein, der in 80% der Anwendungsfälle funktioniert, in denen Sie eine einfache LRU-Karte benötigen.
Ich freue mich über Feedback, außer warum Sie die Bibliothek a, b oder c nicht verwenden. Der Grund, warum ich nicht immer eine Bibliothek verwende, ist, dass ich nicht immer möchte, dass jede War-Datei 80 MB groß ist, und ich schreibe Bibliotheken, sodass ich die Bibliotheken mit einer ausreichend guten Lösung steckbar mache und jemand einstecken kann -in einem anderen Cache-Anbieter, wenn sie möchten. :) Ich weiß nie, wann jemand Guava oder ehcache oder etwas anderes benötigt, das ich nicht einschließen möchte, aber wenn ich das Caching steckbar mache, werde ich sie auch nicht ausschließen.
Die Reduzierung von Abhängigkeiten hat ihre eigene Belohnung. Ich freue mich über Feedback, wie ich dies noch einfacher oder schneller oder beides machen kann.
Auch wenn jemand von einem Ready to Go weiß ....
Ok .. ich weiß was du denkst ... Warum benutzt er nicht einfach den Eintrag removeEldest von LinkedHashMap und nun sollte ich aber ... aber ... aber ... das wäre ein FIFO, kein LRU und wir waren es versuchen, eine LRU zu implementieren.
Map<KEY, VALUE> map = new LinkedHashMap<KEY, VALUE> () {
@Override
protected boolean removeEldestEntry ( Map.Entry<KEY, VALUE> eldest ) {
return this.size () > limit;
}
};
Dieser Test schlägt für den obigen Code fehl ...
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 ();
Hier ist also ein schneller und schmutziger FIFO-Cache mit 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 sind schnell. Keine Suche. Sie könnten ein FIFO vor eine LRU stellen, und das würde die meisten heißen Einträge recht gut handhaben. Eine bessere LRU benötigt dieses Reverse-Element zur Node-Funktion.
Wie auch immer ... jetzt, wo ich Code geschrieben habe, lass mich die anderen Antworten durchgehen und sehen, was ich verpasst habe ... als ich sie das erste Mal gescannt habe.
O(1)
Erforderliche Version: stackoverflow.com/questions/23772102/…