Đây là vòng hai.
Vòng đầu tiên là những gì tôi nghĩ ra sau đó tôi đọc lại các bình luận với tên miền ăn sâu hơn một chút trong đầu.
Vì vậy, đây là phiên bản đơn giản nhất với một bài kiểm tra đơn vị cho thấy nó hoạt động dựa trên một số phiên bản khác.
Đầu tiên là phiên bản không đồng thời:
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 ();
}
}
Cờ thật sẽ theo dõi quyền truy cập của get và put. Xem JavaDocs. RemoveEdelstEntry không có cờ thực sự cho hàm tạo sẽ chỉ thực hiện bộ đệm FIFO (xem ghi chú bên dưới trên FIFO và removeEldestEntry).
Đây là bài kiểm tra chứng minh rằng nó hoạt động như một bộ đệm 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 ();
}
Bây giờ cho phiên bản đồng thời ...
gói 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 ();
}
}
Bạn có thể thấy lý do tại sao tôi bao gồm phiên bản không đồng thời đầu tiên. Các nỗ lực trên để tạo ra một số sọc để giảm sự tranh chấp khóa. Vì vậy, chúng tôi băm khóa và sau đó tìm kiếm băm đó để tìm bộ đệm thực tế. Điều này làm cho kích thước giới hạn nhiều hơn một gợi ý / phỏng đoán sơ bộ trong một lượng lỗi khá lớn tùy thuộc vào mức độ lan truyền của thuật toán băm khóa của bạn.
Dưới đây là thử nghiệm để chỉ ra rằng phiên bản đồng thời có thể hoạt động. :) (Thử nghiệm dưới lửa sẽ là cách thực sự).
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 ();
}
}
Đây là bài đăng cuối cùng .. Bài đăng đầu tiên tôi đã xóa vì nó là LFU không phải là bộ đệm LRU.
Tôi nghĩ rằng tôi sẽ cho điều này một lần nữa đi. Tôi đã cố gắng đưa ra phiên bản đơn giản nhất của bộ đệm LRU bằng cách sử dụng JDK tiêu chuẩn với quá nhiều triển khai.
Đây là những gì tôi đã đưa ra. Nỗ lực đầu tiên của tôi là một chút thảm họa khi tôi thực hiện LFU thay vì và LRU, và sau đó tôi đã thêm hỗ trợ của FIFO và LRU cho nó ... và sau đó tôi nhận ra nó đang trở thành một con quái vật. Sau đó, tôi bắt đầu nói chuyện với John, bạn thân của tôi, người gần như không quan tâm, và sau đó tôi đã mô tả sâu về cách tôi triển khai LFU, LRU và FIFO và cách bạn có thể chuyển đổi nó bằng một đối số ENUM đơn giản, và sau đó tôi nhận ra rằng tất cả những gì tôi thực sự muốn là một LRU đơn giản. Vì vậy, bỏ qua bài đăng trước đó từ tôi và cho tôi biết nếu bạn muốn xem bộ đệm ẩn LRU / LFU / FIFO có thể chuyển đổi thông qua một enum ... không? Được rồi .. anh đi đây.
LRU đơn giản nhất có thể chỉ sử dụng JDK. Tôi đã triển khai cả phiên bản đồng thời và phiên bản không đồng thời.
Tôi đã tạo một giao diện chung (đó là chủ nghĩa tối giản nên có thể thiếu một vài tính năng mà bạn muốn nhưng nó hoạt động cho các trường hợp sử dụng của tôi, nhưng hãy để nếu bạn muốn xem tính năng XYZ hãy cho tôi biết ... Tôi sống để viết mã.) .
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 ();
}
Bạn có thể tự hỏi những gì getSilent là . Tôi sử dụng điều này để thử nghiệm. getSilent không thay đổi điểm số LRU của một mặt hàng.
Đầu tiên là không đồng thời ....
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 ();
}
}
Hàng đợi.removeFirstOccurrence là một hoạt động có khả năng tốn kém nếu bạn có bộ đệm lớn. Người ta có thể lấy LinkedList làm ví dụ và thêm bản đồ băm tra cứu ngược từ phần tử sang nút để thực hiện các thao tác loại bỏ RẤT NHIỀU và phù hợp hơn. Tôi cũng bắt đầu, nhưng rồi nhận ra mình không cần nó. Nhưng ... có lẽ ...
Khi đặt được gọi, khóa được thêm vào hàng đợi. Khi nhận được gọi, khóa sẽ được gỡ bỏ và thêm lại vào đầu hàng đợi.
Nếu bộ nhớ cache của bạn nhỏ và việc xây dựng một mục đắt tiền thì đây sẽ là một bộ đệm tốt. Nếu bộ nhớ cache của bạn thực sự lớn, thì tìm kiếm tuyến tính có thể là cổ chai đặc biệt là nếu bạn không có vùng bộ đệm nóng. Các điểm nóng càng dữ dội, tìm kiếm tuyến tính càng nhanh vì các mục nóng luôn ở đầu tìm kiếm tuyến tính. Dù sao ... những gì cần thiết để làm điều này nhanh hơn là viết một LinkedList khác có thao tác gỡ bỏ có phần tử đảo ngược để tra cứu nút, sau đó xóa sẽ nhanh như xóa một khóa khỏi bản đồ băm.
Nếu bạn có bộ đệm dưới 1.000 mục, điều này sẽ hoạt động tốt.
Đây là một thử nghiệm đơn giản để hiển thị các hoạt động của nó trong hành động.
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 ();
}
}
Bộ nhớ cache LRU cuối cùng được phân luồng đơn và xin vui lòng không bọc nó trong bất kỳ thứ gì được đồng bộ hóa ....
Đây là một đâm ở một phiên bản đồng thời.
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 ();
}
}
Sự khác biệt chính là việc sử dụng ConcảnHashMap thay vì HashMap và sử dụng Khóa (Tôi có thể đã nhận được đồng bộ hóa, nhưng ...).
Tôi chưa thử nó dưới lửa, nhưng có vẻ như bộ đệm LRU đơn giản có thể hoạt động trong 80% trường hợp sử dụng khi bạn cần bản đồ LRU đơn giản.
Tôi hoan nghênh phản hồi, ngoại trừ lý do tại sao bạn không sử dụng thư viện a, b hoặc c. Lý do tôi không luôn sử dụng thư viện là vì tôi không muốn mọi tệp tin chiến tranh là 80 MB và tôi viết thư viện để tôi có thể tạo các libs có thể cắm với một giải pháp đủ tốt và ai đó có thể cắm -Trong nhà cung cấp bộ đệm khác nếu họ thích. :) Tôi không bao giờ biết khi nào ai đó có thể cần Guava hoặc ehcache hoặc thứ gì khác mà tôi không muốn bao gồm chúng, nhưng nếu tôi tạo bộ nhớ đệm có thể cắm, tôi cũng sẽ không loại trừ chúng.
Giảm sự phụ thuộc có phần thưởng riêng của nó. Tôi thích nhận được một số phản hồi về cách làm cho điều này thậm chí đơn giản hơn hoặc nhanh hơn hoặc cả hai.
Ngoài ra nếu có ai biết sẵn sàng để đi ....
Ok .. Tôi biết bạn đang nghĩ gì ... Tại sao anh ấy không sử dụng removeEldest entry từ LinkedHashMap, và tôi cũng nên nhưng .... nhưng .. nhưng đó sẽ là một FIFO không phải là một LRU và chúng tôi đã cố gắng thực hiện một LRU.
Map<KEY, VALUE> map = new LinkedHashMap<KEY, VALUE> () {
@Override
protected boolean removeEldestEntry ( Map.Entry<KEY, VALUE> eldest ) {
return this.size () > limit;
}
};
Thử nghiệm này không thành công cho mã trên ...
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 ();
Vì vậy, đây là một bộ đệm FIFO nhanh và bẩn bằng cách sử dụng 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 là nhanh chóng. Không tìm kiếm xung quanh. Bạn có thể đứng trước một FIFO trước một LRU và điều đó sẽ xử lý hầu hết các mục nóng khá độc đáo. Một LRU tốt hơn sẽ cần yếu tố đảo ngược đó đến tính năng Node.
Dù sao ... bây giờ tôi đã viết một số mã, hãy để tôi xem qua các câu trả lời khác và xem những gì tôi đã bỏ lỡ ... lần đầu tiên tôi quét chúng.
O(1)
phiên bản bắt buộc: stackoverflow.com/questions/23772102/