นี่คือรอบที่สอง
รอบแรกเป็นสิ่งที่ฉันคิดขึ้นมาจากนั้นฉันอ่านความคิดเห็นกับโดเมนอีกครั้งในหัวของฉัน
ดังนั้นนี่คือรุ่นที่ง่ายที่สุดด้วยการทดสอบหน่วยที่แสดงว่าทำงานได้ตามรุ่นอื่น ๆ
รุ่นแรกที่ไม่เกิดขึ้นพร้อมกัน:
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 ();
}
}
ธงที่แท้จริงจะติดตามการเข้าถึงของได้รับและทำให้ ดู JavaDocs removeEdelstEntry ที่ไม่มีแฟล็กที่แท้จริงกับตัวสร้างจะใช้แคช FIFO (ดูหมายเหตุด้านล่างเกี่ยวกับ FIFO และ removeEldestEntry)
นี่คือการทดสอบที่พิสูจน์ว่าทำงานเป็นแคช 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 ();
}
ตอนนี้สำหรับรุ่นที่เกิดขึ้นพร้อมกัน ...
แพคเกจ 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 ();
}
}
คุณสามารถดูว่าทำไมฉันถึงครอบคลุมรุ่นที่ไม่เกิดขึ้นพร้อมกันก่อน ความพยายามด้านบนเพื่อสร้างลายเส้นบางส่วนเพื่อลดความขัดแย้งในการล็อก ดังนั้นเราจึงซ่อนกุญแจและค้นหาแฮชนั้นเพื่อค้นหาแคชจริง สิ่งนี้ทำให้ขนาดข้อ จำกัด มากขึ้นสำหรับข้อเสนอแนะ / การคาดเดาคร่าวๆภายในข้อผิดพลาดจำนวนพอสมควรทั้งนี้ขึ้นอยู่กับว่าแฮชอัลกอริธึมแฮชของคีย์ของคุณกระจายกันอย่างไร
นี่คือการทดสอบเพื่อแสดงให้เห็นว่ารุ่นที่ทำงานพร้อมกันอาจใช้งานได้ :) (ทดสอบภายใต้ไฟจะเป็นวิธีจริง)
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 ();
}
}
นี่เป็นโพสต์สุดท้าย .. โพสต์แรกที่ฉันลบเนื่องจากเป็น LFU ไม่ใช่แคช LRU
ฉันคิดว่าฉันจะให้มันไปอีก ฉันพยายามพยายามหาแคชรุ่น LRU ที่ง่ายที่สุดโดยใช้มาตรฐาน JDK ที่ไม่มีการใช้งานมากเกินไป
นี่คือสิ่งที่ฉันมาด้วย ความพยายามครั้งแรกของฉันคือความหายนะเล็กน้อยเมื่อฉันติดตั้ง LFU และ LRU จากนั้นฉันก็เพิ่ม FIFO และ LRU ก็สนับสนุนมัน ... แล้วฉันก็รู้ว่ามันกลายเป็นสัตว์ประหลาด จากนั้นฉันก็เริ่มพูดคุยกับเพื่อนของฉันจอห์นผู้ที่แทบจะไม่สนใจแล้วฉันอธิบายในเชิงลึกว่าฉันใช้ LFU, LRU และ FIFO ได้อย่างไรและคุณจะสลับมันด้วย ARUM แบบง่าย ๆ ได้อย่างไรและจากนั้นฉันก็ตระหนักว่าทั้งหมดที่ฉันต้องการ เป็น LRU ง่าย ๆ ดังนั้นละเว้นโพสต์ก่อนหน้าจากฉันและแจ้งให้เราทราบหากคุณต้องการดูแคช LRU / LFU / FIFO ที่สามารถสลับได้ผ่าน enum ... ไม่? ตกลง .. ไปเลย
LRU ที่ง่ายที่สุดที่เป็นไปได้โดยใช้เพียง JDK ฉันใช้ทั้งรุ่นที่เกิดขึ้นพร้อมกันและรุ่นที่ไม่เกิดขึ้นพร้อมกัน
ฉันสร้างอินเทอร์เฟซทั่วไป (เป็นแบบมินิมอลดังนั้นอาจขาดคุณสมบัติบางอย่างที่คุณต้องการ แต่ใช้งานได้กับกรณีการใช้งานของฉัน แต่ถ้าคุณต้องการดูฟีเจอร์ XYZ แจ้งให้เราทราบ ... ฉันมีชีวิตอยู่เพื่อเขียนโค้ด) .
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คืออะไร ฉันใช้สิ่งนี้เพื่อทดสอบ getSilent ไม่เปลี่ยนคะแนน LRU ของรายการ
ครั้งแรกที่ไม่เกิดขึ้นพร้อมกันหนึ่ง ....
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 คือการดำเนินการอาจมีราคาแพงถ้าคุณมีแคชขนาดใหญ่ หนึ่งสามารถนำ LinkedList มาเป็นตัวอย่างและเพิ่มแฮชการค้นหาย้อนกลับจากองค์ประกอบหนึ่งไปอีกโหนดเพื่อทำการลบ A ล็อตที่เร็วขึ้นและสอดคล้องกันมากขึ้น ฉันเริ่มด้วย แต่แล้วฉันก็รู้ว่าฉันไม่ต้องการมัน แต่ ... อาจจะ ...
เมื่อใส่เรียกว่ากุญแจสำคัญในการได้รับการเพิ่มลงในคิว เมื่อได้รับการเรียกว่ากุญแจสำคัญในการได้รับการถอดออกและใหม่เพิ่มไปยังด้านบนของคิว
หากแคชของคุณมีขนาดเล็กและการสร้างรายการมีราคาแพงก็ควรเป็นแคชที่ดี หากแคชของคุณมีขนาดใหญ่จริงๆการค้นหาเชิงเส้นอาจเป็นคอขวดโดยเฉพาะถ้าคุณไม่มีพื้นที่แคชร้อน ยิ่งฮอตสปอตยิ่งเข้มขึ้นเท่าไหร่การค้นหาเชิงเส้นก็จะยิ่งรวดเร็วมากขึ้นเท่านั้นเมื่อไอเท็มยอดนิยมนั้นอยู่ด้านบนสุดของการค้นหาเชิงเส้นเสมอ อย่างไรก็ตาม ... สิ่งที่จำเป็นสำหรับการดำเนินการที่รวดเร็วกว่านี้คือการเขียน LinkedList อีกอันที่มีการดำเนินการลบที่มีองค์ประกอบย้อนกลับไปยังการค้นหาโหนดเพื่อลบจากนั้นการลบจะเร็วพอ ๆ กับการลบคีย์ออกจากแผนที่แฮช
หากคุณมีแคชต่ำกว่า 1,000 รายการสิ่งนี้น่าจะใช้ได้จริง
นี่คือการทดสอบง่ายๆเพื่อแสดงการทำงานของมัน
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 ();
}
}
แคช LRU ที่ผ่านมาเป็นเธรดเดียวและโปรดอย่าห่อมันในสิ่งที่ทำข้อมูลให้ตรงกัน ....
นี่คือแทงที่รุ่นที่เกิดขึ้นพร้อมกัน
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 ();
}
}
ความแตกต่างที่สำคัญคือการใช้ ConcurrentHashMap แทน HashMap และการใช้ Lock (ฉันอาจได้รับการซิงโครไนซ์ แต่ ... )
ฉันไม่ได้ทำการทดสอบภายใต้ไฟ แต่ดูเหมือนว่าแคช LRU แบบง่าย ๆ ที่อาจใช้งานได้ 80% ของกรณีการใช้งานที่คุณต้องการแผนที่ LRU แบบง่าย ๆ
ฉันยินดีรับข้อเสนอแนะยกเว้นทำไมคุณไม่ใช้ไลบรารี a, b หรือ c เหตุผลที่ฉันไม่ได้ใช้ห้องสมุดเสมอเพราะฉันไม่ต้องการให้ไฟล์สงครามทุกไฟล์เป็น 80MB และฉันเขียนไลบรารีดังนั้นฉันมักจะทำให้ libs plug-can พร้อมทางออกที่ดีพอและใครบางคนสามารถเสียบ - ในผู้ให้บริการแคชอื่นหากพวกเขาต้องการ :) ฉันไม่เคยรู้เลยว่าใครบางคนอาจต้องการ Guava หรือ ehcache หรืออย่างอื่นฉันไม่ต้องการรวมพวกเขา แต่ถ้าฉันทำการแคชปลั๊กอินฉันจะไม่ยกเว้นพวกเขาเช่นกัน
การลดการพึ่งพามีรางวัลเป็นของตัวเอง ฉันชอบที่จะได้รับคำติชมเกี่ยวกับวิธีทำให้เรื่องนี้ง่ายขึ้นหรือเร็วขึ้นหรือทั้งสองอย่าง
ถ้าใครรู้ว่าพร้อมที่จะไป ....
ตกลง .. ฉันรู้ว่าคุณกำลังคิดอะไรอยู่ ... ทำไมเขาไม่ใช้รายการ RemoveEldest จาก LinkedHashMap และดีที่ฉันควรจะ แต่ .... แต่ .. แต่ .. แต่ .. นั่นจะเป็น FIFO ไม่ใช่ LRU และพวกเรา พยายามใช้ LRU
Map<KEY, VALUE> map = new LinkedHashMap<KEY, VALUE> () {
@Override
protected boolean removeEldestEntry ( Map.Entry<KEY, VALUE> eldest ) {
return this.size () > limit;
}
};
การทดสอบนี้ล้มเหลวสำหรับรหัสข้างต้น ...
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 ();
ดังนั้นนี่คือแคช FIFO ที่รวดเร็วและสกปรกโดยใช้ 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 ();
}
}
FIFO รวดเร็ว ไม่มีการค้นหารอบ ๆ คุณสามารถหน้า FIFO ต่อหน้า LRU และนั่นจะจัดการกับรายการที่ร้อนแรงที่สุด LRU ที่ดีขึ้นจะต้องใช้องค์ประกอบย้อนกลับไปยังคุณสมบัติของโหนด
อย่างไรก็ตาม ... ตอนนี้ฉันเขียนโค้ดให้ฉันอ่านคำตอบและดูว่าฉันพลาดอะไรไป ... ครั้งแรกที่ฉันสแกนมัน
O(1)
รุ่นที่ต้องการ: stackoverflow.com/questions/23772102/…