이것은 2 라운드입니다.
첫 번째 라운드는 내가 생각해 낸 다음 내 머리에 조금 더 뿌리 박힌 도메인의 의견을 다시 읽었습니다.
다음은 다른 버전을 기반으로 작동하는 단위 테스트를 사용한 가장 간단한 버전입니다.
먼저 비 동시 버전 :
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 ();
}
}
true 플래그는 가져 오기 및 넣기 액세스를 추적합니다. JavaDocs를 참조하십시오. 생성자에 대한 true 플래그가없는 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 ();
}
}
이것이 마지막 게시물입니다. LRU 캐시가 아닌 LFU이므로 처음 삭제 한 게시물입니다.
나는 이것을 또 다른 것으로 줄 것이라고 생각했다. 너무 많은 구현이없는 표준 JDK를 사용하여 가장 간단한 LRU 캐시 버전을 만들려고했습니다.
여기에 내가 생각해 낸 것이 있습니다. 첫 번째 시도는 LRU 대신 LFU를 구현 한 후 약간의 재앙이었고 FIFO 및 LRU 지원을 추가하여 괴물이되고 있음을 깨달았습니다. 그런 다음 간신히 관심이있는 내 친구 인 John과 대화를 시작한 후 LFU, LRU 및 FIFO를 구현 한 방법과 간단한 ENUM 인수로 전환 할 수있는 방법에 대해 깊이있게 설명한 다음, 제가 원했던 모든 것을 깨달았습니다 간단한 LRU였습니다. 따라서 이전 게시물을 무시하고 열거 형을 통해 전환 할 수있는 LRU / LFU / FIFO 캐시를보고 싶다면 알려주십시오. 알았어
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 대규모 캐시가있는 경우 잠재적으로 비용이 많이 드는 작업이다. LinkedList를 예로 들어 요소에서 노드로 역 조회 해시 맵을 추가하여 제거 작업을보다 신속하고 일관성있게 만들 수 있습니다. 나도 시작했지만 필요하지 않다는 것을 깨달았다. 그러나 아마도...
때 풋이 라고, 키는 큐에 추가됩니다. 얻을 때 호출되면 키가 제거됩니다 큐의 상단에 다시는-했다.
캐시가 작고 아이템을 만드는 데 비용이 많이 든다면 캐시가 좋습니다. 캐시가 실제로 큰 경우, 특히 캐시 공간이 부족한 경우 선형 검색이 병목 현상이 될 수 있습니다. 핫스팟이 강렬할수록 핫 항목이 항상 선형 검색의 맨 위에 있으므로 선형 검색이 더 빨라집니다. 어쨌든 ... 이보다 빠르게 진행되는 데 필요한 것은 remove에 대한 노드 조회를 역으로 수행하는 remove 작업이있는 다른 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 ();
}
}
주요 차이점은 HashMap 대신 ConcurrentHashMap을 사용하고 Lock을 사용한다는 것입니다 (동기화로 벗어날 수는 있지만 ...).
나는 그것을 테스트하지 않았지만 간단한 LRU 맵이 필요한 사용 사례의 80 %에서 작동 할 수있는 간단한 LRU 캐시처럼 보입니다.
라이브러리 a, b 또는 c를 사용하지 않는 이유를 제외하고 피드백을 환영합니다. 항상 라이브러리를 사용하지 않는 이유는 모든 war 파일이 항상 80MB가되기를 원하지 않기 때문에 라이브러리를 작성하기 때문에 라이브러리를 작성하여 충분한 솔루션을 사용하여 libs를 플러그인 가능하게 만들고 누군가가 플러그인 할 수 있기 때문입니다. 원하는 경우 다른 캐시 공급자에서-. :) 언제 누군가 Guava 또는 ehcache 또는 다른 것을 포함하고 싶지 않은지 알지 못하지만 캐싱을 가능하게 만들면 제외하지 않습니다.
의존성 감소에는 자체 보상이 있습니다. 나는 이것을 더 간단하거나 빠르게 만드는 방법에 대한 피드백을 받고 싶습니다.
또한 갈 준비가 된 사람이 있다면 ...
알았어 .. 네가 무슨 생각을하는지 알아 .. 왜 LinkedHashMap에서 removeEldest 항목을 사용하지 않는가?하지만 잘해야하지만 ..하지만 ...... 그건 LRU가 아닌 FIFO 일 것이고 우리는 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 ();
}
}
FIFO는 빠릅니다. 주변을 검색하지 않습니다. LRU 앞에 선입 선출 (FIFO)을 앞당길 수 있으며 대부분의 핫 엔트리를 아주 잘 처리 할 수 있습니다. 더 나은 LRU는 노드 기능에 대한 역방향 요소가 필요합니다.
어쨌든 ... 이제 코드를 작성 했으므로 다른 답변을 살펴보고 처음으로 스캔 한 내용을 보도록하겠습니다.
O(1)
필요한 버전 : stackoverflow.com/questions/23772102/...