यह दो का दौर है।
पहला दौर वह था जिसके साथ मैं आया था, फिर मैंने अपने सिर में और अधिक घिरे हुए डोमेन के साथ टिप्पणियों को फिर से लिखा।
तो यहां एक यूनिट टेस्ट के साथ सबसे सरल संस्करण है जो दिखाता है कि यह कुछ अन्य संस्करणों के आधार पर काम करता है।
पहला गैर-समवर्ती संस्करण:
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 देखें। निर्माता को सही ध्वज के बिना निष्कासन को हटाए बस एक FIFO कैश लागू होगा (FIFO पर नीचे दिए गए नोट्स और हटाने के लिए देखें)।
यहाँ परीक्षण है कि यह साबित होता है कि यह 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 ();
}
}
यह आखिरी पोस्ट है .. पहली पोस्ट मैंने डिलीट कर दी थी क्योंकि यह एलएफयू थी न कि एलआरयू कैश।
मैंने सोचा कि मैं इसे दूसरा रास्ता दूंगा। मैं मानक JDK w / o बहुत अधिक कार्यान्वयन का उपयोग करके LRU कैश के सबसे सरल संस्करण के साथ आने की कोशिश कर रहा था।
यहां वह है जो मैंने जुटाया। मेरा पहला प्रयास एक आपदा का था क्योंकि मैंने और एलआरयू के बजाय एलएफयू को लागू किया था, और फिर मैंने इसमें फीफो और एलआरयू का समर्थन जोड़ा ... और फिर मुझे एहसास हुआ कि यह एक राक्षस बन रहा है। फिर मैंने अपने दोस्त जॉन से बात करना शुरू कर दिया, जो मुश्किल से दिलचस्पी ले रहा था, और फिर मैंने गहरी लंबाई में बताया कि मैंने एक एलएफयू, एलआरयू और एफआईएफओ को कैसे लागू किया और आप इसे एक सरल ईनम आर्ग के साथ कैसे स्विच कर सकते हैं, और तब मुझे एहसास हुआ कि सभी वास्तव में चाहते थे एक साधारण LRU था। तो मेरे से पहले की पोस्ट को अनदेखा करें, और मुझे बताएं कि क्या आप एक LRU / LFU / FIFO कैश देखना चाहते हैं जो कि एनम के माध्यम से switchable है ... नहीं? ठीक है .. यहाँ वह जाओ।
केवल JDK का उपयोग करके सबसे सरल संभव LRU। मैंने एक समवर्ती संस्करण और एक गैर-समवर्ती संस्करण दोनों को लागू किया।
मैंने एक सामान्य इंटरफ़ेस बनाया (यह अतिसूक्ष्मवाद है इसलिए कुछ विशेषताओं को याद नहीं किया जा सकता है जो आप चाहेंगे लेकिन यह मेरे उपयोग के मामलों के लिए काम करता है, लेकिन यदि आप सुविधा 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 किसी आइटम का 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 यदि आप किसी बड़े कैश है एक संभावित महंगा ऑपरेशन है। एक लिंक्डलिस्ट को एक उदाहरण के रूप में ले सकता है और एक ऑपरेशन को हटाने और अधिक सुसंगत बनाने के लिए तत्व से नोड तक रिवर्स लुकअप हैश मैप को जोड़ सकता है। मैंने भी शुरुआत की, लेकिन तब मुझे महसूस हुआ कि मुझे इसकी आवश्यकता नहीं है। लेकिन हो सकता है...
जब डाल कहा जाता है, कुंजी कतार में जोड़ा जाता है। कब मिलेगा? कहा जाता है, कुंजी हटा दिया जाता है और कतार में शीर्ष पर फिर से जोड़ दिया।
यदि आपका कैश छोटा है और किसी वस्तु का निर्माण महंगा है तो यह एक अच्छा कैश होना चाहिए। यदि आपका कैश वास्तव में बड़ा है, तो रैखिक खोज एक बोतल गर्दन हो सकती है, खासकर यदि आपके पास कैश के गर्म क्षेत्र नहीं हैं। गर्म स्थान जितना अधिक तीव्र होता है, गर्म वस्तुओं के रूप में उतनी ही तेजी से रैखिक खोज हमेशा रैखिक खोज के शीर्ष पर होती है। वैसे भी ... इसके लिए तेजी से जाने के लिए क्या आवश्यक है एक और लिंक्डलिस्ट लिखिए जिसमें एक हटाने का ऑपरेशन होता है जिसमें हटाने के लिए नोड लुकअप करने के लिए उल्टा तत्व होता है, फिर एक हैश मैप से एक कुंजी को हटाने के रूप में हटाने के रूप में तेजी से होगा।
यदि आपके पास 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 ();
}
}
मुख्य अंतर हैशपॉप के बजाय कॉनकंट्रैशहैश का उपयोग है, और लॉक का उपयोग (मुझे सिंक्रनाइज़ के साथ दूर हो सकता है, लेकिन ...)।
मैंने इसे आग के तहत परीक्षण नहीं किया है, लेकिन यह एक साधारण LRU कैश की तरह लगता है जो 80% उपयोग के मामलों में काम कर सकता है जहां आपको एक साधारण LRU मानचित्र की आवश्यकता होती है।
मैं प्रतिक्रिया का स्वागत करता हूं, सिवाय इसके कि आप लाइब्रेरी ए, बी या सी का उपयोग क्यों नहीं करते हैं। मेरे द्वारा हमेशा लाइब्रेरी का उपयोग न करने का कारण यह है क्योंकि मैं हमेशा नहीं चाहता कि हर युद्ध की फाइल 80 एमबी की हो, और मैं लाइब्रेरी लिखता हूं, इसलिए मैं जगह में अच्छे पर्याप्त समाधान के साथ लिबास को प्लग-इन करने में सक्षम हूं और कोई भी प्लग कर सकता है एक और कैश प्रदाता में अगर वे पसंद करते हैं। :) मैं कभी नहीं जानता कि कब किसी को अमरूद या ईचेचे या कुछ और चाहिए जो मैं उन्हें शामिल नहीं करना चाहता, लेकिन अगर मैं कैशिंग प्लग-इन करूं, तो मैं उन्हें बाहर नहीं करूंगा।
निर्भरता कम करने का अपना प्रतिफल होता है। मैं इसे और भी सरल या तेज या दोनों बनाने के लिए कुछ प्रतिक्रिया प्राप्त करना पसंद करता हूं।
साथ ही अगर कोई जाने के लिए तैयार है ...।
ठीक है .. मुझे पता है कि आप क्या सोच रहे हैं ... वह लिंक्डहैशपाइप से हटाने की सबसे बड़ी प्रविष्टि का उपयोग क्यों नहीं करता है, और अच्छी तरह से मुझे करना चाहिए लेकिन .... लेकिन .. लेकिन .. यह एक फीफो नहीं एक 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 ();
तो यहाँ removeEldestEntry का उपयोग करके एक त्वरित और गंदा FIFO कैश है।
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 ();
}
}
एफआईएफओ तेज हैं। आसपास कोई खोज नहीं। आप एक LRU के सामने एक FIFO लगा सकते हैं और यह सबसे गर्म प्रविष्टियों को काफी अच्छी तरह से हैंडल करेगा। एक बेहतर एलआरयू को उस उल्टे तत्व को नोड सुविधा की आवश्यकता होने जा रही है।
वैसे भी ... अब जब मैंने कुछ कोड लिखा है, तो मुझे अन्य उत्तरों के माध्यम से जाने दें और देखें कि मैंने क्या याद किया ... पहली बार मैंने उन्हें स्कैन किया।
O(1)
आवश्यक संस्करण: stackoverflow.com/questions/23772102/…