Ini babak dua.
Babak pertama adalah apa yang saya hasilkan kemudian saya membaca kembali komentar dengan domain yang sedikit lebih tertanam di kepala saya.
Jadi di sini adalah versi paling sederhana dengan tes unit yang menunjukkan itu berfungsi berdasarkan beberapa versi lainnya.
Pertama versi non-konkuren:
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 ();
}
}
Bendera yang sebenarnya akan melacak akses dari mendapat dan menempatkan. Lihat JavaDocs. RemoveEdelstEntry tanpa flag yang sebenarnya ke konstruktor hanya akan mengimplementasikan cache FIFO (lihat catatan di bawah tentang FIFO dan removeEldestEntry).
Berikut adalah tes yang membuktikannya berfungsi sebagai cache 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 ();
}
Sekarang untuk versi bersamaan ...
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 ();
}
}
Anda dapat melihat mengapa saya membahas versi non-konkuren terlebih dahulu. Upaya di atas untuk membuat beberapa garis untuk mengurangi pertikaian kunci. Jadi kita hash kunci dan kemudian mencari hash untuk menemukan cache yang sebenarnya. Ini membuat ukuran batas lebih dari saran / tebakan kasar dalam jumlah kesalahan yang wajar tergantung pada seberapa baik penyebaran algoritma hash kunci Anda.
Berikut adalah tes untuk menunjukkan bahwa versi bersamaan mungkin berfungsi. :) (Tes di bawah api akan menjadi cara nyata).
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 ();
}
}
Ini adalah posting terakhir .. Posting pertama yang saya hapus karena itu adalah LFU bukan cache LRU.
Saya pikir saya akan mencoba ini lagi. Saya mencoba untuk membuat versi paling sederhana dari cache LRU menggunakan standar JDK tanpa implementasi terlalu banyak.
Inilah yang saya pikirkan. Upaya pertama saya adalah sedikit bencana ketika saya menerapkan LFU dan bukannya LRU, dan kemudian saya menambahkan FIFO, dan dukungan LRU untuk itu ... dan kemudian saya menyadari itu menjadi monster. Kemudian saya mulai berbicara dengan teman saya John yang hampir tidak tertarik, dan kemudian saya jelaskan panjang lebar bagaimana saya menerapkan LFU, LRU dan FIFO dan bagaimana Anda bisa mengubahnya dengan arg ENUM sederhana, dan kemudian saya menyadari bahwa semua yang saya inginkan adalah LRU sederhana. Jadi abaikan pos sebelumnya dari saya, dan beri tahu saya jika Anda ingin melihat cache LRU / LFU / FIFO yang dapat dialihkan melalui enum ... tidak? Ok .. ini dia.
LRU yang paling sederhana dengan JDK. Saya menerapkan versi bersamaan dan versi tidak bersamaan.
Saya membuat antarmuka umum (ini minimalis sehingga kemungkinan kehilangan beberapa fitur yang Anda inginkan tetapi berfungsi untuk kasus penggunaan saya, tetapi biarkan jika Anda ingin melihat fitur XYZ beri tahu saya ... Saya hidup untuk menulis kode.) .
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 ();
}
Anda mungkin bertanya-tanya apa itu getSilent . Saya menggunakan ini untuk pengujian. getSilent tidak mengubah skor LRU dari suatu item.
Pertama yang tidak bersamaan ....
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 ();
}
}
The queue.removeFirstOccurrence adalah operasi berpotensi mahal jika Anda memiliki cache yang besar. Orang bisa mengambil LinkedList sebagai contoh dan menambahkan peta hash reverse lookup dari elemen ke node untuk membuat operasi penghapusan BANYAK LEBIH CEPAT dan lebih konsisten. Saya mulai juga, tetapi kemudian menyadari bahwa saya tidak membutuhkannya. Tapi mungkin...
Ketika put dipanggil, kunci akan ditambahkan ke antrian. Ketika mendapatkan disebut, kunci akan dihapus dan kembali ditambahkan ke bagian atas dari antrian.
Jika cache Anda kecil dan membuat item mahal maka ini harus menjadi cache yang baik. Jika cache Anda benar-benar besar, maka pencarian linier bisa menjadi leher botol terutama jika Anda tidak memiliki area cache yang panas. Semakin banyak hot spot, semakin cepat pencarian linier karena item panas selalu berada di bagian atas pencarian linear. Ngomong-ngomong ... apa yang diperlukan agar ini berjalan lebih cepat adalah menulis LinkedList lain yang memiliki operasi hapus yang memiliki elemen terbalik ke pencarian simpul untuk dihapus, kemudian menghapusnya akan secepat menghapus kunci dari peta hash.
Jika Anda memiliki cache di bawah 1.000 item, ini akan bekerja dengan baik.
Berikut ini adalah tes sederhana untuk menunjukkan operasinya dalam tindakan.
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 ();
}
}
Cache LRU terakhir adalah utas tunggal, dan tolong jangan membungkusnya dengan sinkronisasi apa pun ....
Ini adalah tikaman pada versi bersamaan.
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 ();
}
}
Perbedaan utama adalah penggunaan ConcurrentHashMap, bukan HashMap, dan penggunaan Lock (saya bisa lolos dengan disinkronkan, tapi ...).
Saya belum mengujinya di bawah api, tetapi sepertinya cache LRU sederhana yang mungkin berhasil di 80% kasus penggunaan di mana Anda memerlukan peta LRU sederhana.
Saya menyambut umpan balik, kecuali mengapa Anda tidak menggunakan perpustakaan a, b, atau c. Alasan saya tidak selalu menggunakan perpustakaan adalah karena saya tidak selalu ingin setiap file perang menjadi 80MB, dan saya menulis perpustakaan jadi saya cenderung membuat lib plug-mampu dengan solusi yang cukup bagus di tempat dan seseorang dapat memasang -di penyedia cache lain jika mereka suka. :) Saya tidak pernah tahu kapan seseorang mungkin membutuhkan Guava atau ehcache atau sesuatu yang lain yang saya tidak ingin sertakan, tetapi jika saya membuat caching plug-mampu, saya tidak akan mengecualikan mereka juga.
Pengurangan ketergantungan memiliki imbalannya sendiri. Saya suka mendapatkan umpan balik tentang cara menjadikan ini lebih sederhana atau lebih cepat atau keduanya.
Juga jika ada yang tahu siap untuk pergi ....
Ok .. Saya tahu apa yang Anda pikirkan ... Mengapa dia tidak hanya menggunakan entri removeEldest dari LinkedHashMap, dan saya harus tetapi .... tapi .. tapi .. Itu akan menjadi FIFO bukan LRU dan kami mencoba menerapkan LRU.
Map<KEY, VALUE> map = new LinkedHashMap<KEY, VALUE> () {
@Override
protected boolean removeEldestEntry ( Map.Entry<KEY, VALUE> eldest ) {
return this.size () > limit;
}
};
Tes ini gagal untuk kode di atas ...
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 ();
Jadi di sini adalah cache FIFO yang cepat dan kotor menggunakan 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 cepat. Tidak mencari-cari. Anda dapat mem-depan FIFO di depan LRU dan itu akan menangani sebagian besar entri panas dengan cukup baik. LRU yang lebih baik akan membutuhkan elemen pembalik untuk fitur Node.
Ngomong-ngomong ... sekarang saya menulis beberapa kode, biarkan saya memeriksa jawaban lain dan melihat apa yang saya lewatkan ... pertama kali saya memindai mereka.
O(1)
versi yang diperlukan: stackoverflow.com/questions/23772102/…