Bu ikinci tur.
İlk tur, ortaya çıkardığım şeydi, sonra yorumları kafamda biraz daha kökleşmiş olan alanlarla tekrar okudum.
İşte burada, bazı diğer sürümlere göre çalıştığını gösteren bir birim testi ile en basit sürüm.
İlk olarak eşzamanlı olmayan sürüm:
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 ();
}
}
Gerçek bayrak, alma ve koyma erişimini izler. Bkz. JavaDocs. Yapıcıya gerçek bayrağı olmadan removeEdelstEntry yalnızca bir FIFO önbelleği uygular (aşağıdaki FIFO ve removeEldestEntry ile ilgili notlara bakın).
İşte LRU önbellek olarak çalıştığını kanıtlayan test:
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 ();
}
Şimdi eşzamanlı sürüm için ...
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 ();
}
}
Öncelikle eşzamanlı olmayan sürümü neden kapsadığımı görebilirsiniz. Yukarıda kilit çekişmeyi azaltmak için bazı şeritler oluşturmaya çalışır. Bu yüzden anahtarı hash eder ve sonra gerçek önbelleği bulmak için bu hash arar. Bu, anahtar boyutunu karma algoritmasının ne kadar iyi yayıldığına bağlı olarak, sınır boyutunu makul miktarda hata içinde bir öneri / kaba tahminden daha fazla yapar.
Eşzamanlı sürümün muhtemelen çalıştığını gösteren test. :) (Ateş altında test gerçek yol olurdu).
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 ();
}
}
Bu son gönderi .. LRU önbellek değil LFU olduğu için sildiğim ilk gönderi.
Bunu bir kez daha vereceğimi düşündüm. Standart JDK çok fazla uygulama kullanarak bir LRU önbelleğinin en basit sürümünü bulmaya çalışıyordum.
İşte ortaya çıkardığım şey. İlk denemem bir LFU yerine ve LRU yerine biraz felaket oldu ve sonra FIFO ve LRU desteği ekledim ... ve sonra bir canavar haline geldiğini fark ettim. Sonra zorlukla ilgilenen arkadaşım John'la konuşmaya başladım ve daha sonra LFU, LRU ve FIFO'yu nasıl uyguladığımı ve basit bir ENUM argümanıyla nasıl değiştirebileceğinizi açıkladım ve sonra gerçekten istediğimi fark ettim basit bir LRU idi. Bu yüzden benden önceki gönderiyi görmezden gelin ve bir numaralandırma yoluyla değiştirilebilir bir LRU / LFU / FIFO önbelleği görmek istiyorsanız bana bildirin ... hayır? Tamam .. işte gidiyor.
Sadece JDK kullanarak mümkün olan en basit LRU. Hem eşzamanlı hem de eşzamanlı olmayan bir sürüm uyguladım.
Ortak bir arayüz oluşturdum (minimalizm çok büyük bir olasılıkla istediğiniz birkaç özelliği eksik ama kullanım durumlarım için çalışıyor, ancak XYZ özelliğini görmek isterseniz izin verin bana bildirin ... Kod yazmak için yaşıyorum.) .
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 ();
}
GetSilent'in ne olduğunu merak edebilirsiniz . Bunu test için kullanıyorum. getSilent bir öğenin LRU puanını değiştirmez.
Önce eşzamanlı olmayan ....
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 Büyük bir önbellek varsa potansiyel olarak pahalı bir işlemdir. Bir örnek olarak LinkedList alabilir ve kaldırma işlemleri çok daha hızlı ve daha tutarlı hale getirmek için öğeden düğüme bir ters arama karma haritası eklenebilir. Ben de başladım ama sonra ihtiyacım olmadığını fark ettim. Ama belki...
Ne zaman koymak denir, anahtar kuyruğuna eklenir. Ne zaman olsun denir, anahtar kaldırılırsa ve sıranın üstüne yeniden eklendi.
Önbelleğiniz küçükse ve bir öğe oluşturmak pahalıysa, bu iyi bir önbellek olmalıdır. Önbelleğiniz gerçekten büyükse, özellikle sıcak önbellek alanlarınız yoksa doğrusal arama bir şişe boynu olabilir. Sıcak noktalar ne kadar yoğun olursa, sıcak öğeler her zaman doğrusal aramanın en üstünde olduğu için doğrusal arama o kadar hızlı olur. Her neyse ... bunun daha hızlı gitmesi için gereken şey, kaldırmak için düğüm aramasına ters eleman içeren bir kaldırma işlemi olan başka bir LinkedList yazmaktır, daha sonra kaldırma, bir karma haritasından bir anahtarı kaldırmak kadar hızlı olur.
1.000 öğenin altında bir önbelleğe sahipseniz, bunun iyi olması gerekir.
İşte operasyonlarını eylem halinde göstermek için basit bir test.
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 ();
}
}
Son LRU önbellek tek iş parçacıklı idi ve lütfen senkronize bir şey sarmayın ....
Eşzamanlı versiyonda bir bıçak.
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 ();
}
}
Temel farklılıklar HashMap yerine ConcurrentHashMap kullanımı ve Lock kullanımıdır (senkronize edilmiş olabilirim, ama ...).
Ateş altında test etmedim, ancak basit bir LRU haritasına ihtiyacınız olan kullanım durumlarının% 80'inde çalışabilecek basit bir LRU önbelleği gibi görünüyor.
Neden a, b veya c kitaplığını kullanmamanız dışında geri bildirim almaktan memnuniyet duyuyorum. Her zaman bir kütüphane kullanmamamın nedeni, her savaş dosyasının her zaman 80MB olmasını istememem ve kütüphaneler yazmamdan dolayı kütüphaneleri yerinde yeterince iyi bir çözümle takılabilir hale getirme eğilimindeyim ve birisi takabilir -eğer isterse başka bir önbellek sağlayıcısında. :) Birisinin ne zaman Guava veya ehcache'ye veya başka bir şeye ihtiyaç duyduğunu asla bilemeyeceğim, ancak bunları dahil etmek istemiyorum, ancak önbelleğe alma eklentisini yaparsam, onları da hariç tutmayacağım.
Bağımlılıkların azaltılmasının kendi ödülü vardır. Bunu nasıl daha basit, daha hızlı veya her ikisini birden yapabileceğiniz hakkında geri bildirim almayı seviyorum.
Ayrıca kimse gitmek için hazır bir biliyorsa ....
Tamam .. Ne düşündüğünü biliyorum ... Neden sadece LinkedHashMap'ten removeEldest girdisini kullanmıyor ve ben yapmalıyım ama .... ama .. ama .. Bu bir LRU değil bir FIFO olurdu ve biz LRU uygulamaya çalışıyor.
Map<KEY, VALUE> map = new LinkedHashMap<KEY, VALUE> () {
@Override
protected boolean removeEldestEntry ( Map.Entry<KEY, VALUE> eldest ) {
return this.size () > limit;
}
};
Bu test yukarıdaki kod için başarısız oluyor ...
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 ();
Burada removeEldestEntry kullanarak hızlı ve kirli bir FIFO önbelleği var.
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 ();
}
}
FIFO'lar hızlı. Etrafta arama yok. Bir LRU önünde bir FIFO ön olabilir ve bu çoğu sıcak girişleri oldukça güzel idare edecek. Daha iyi bir LRU, bu ters elemanı Düğüm özelliğine ihtiyaç duyacaktır.
Her neyse ... şimdi bir kod yazdığım için, diğer cevapları gözden geçirmeme ve neyi kaçırdığımı görmeme izin verin ... onları ilk taradığımda.
O(1)
gerekli sürüm: stackoverflow.com/questions/23772102/…