Как бы вы реализовали кэш LRU в Java?
пожалуйста, не говорите EHCache или OSCache и т. д. Предположим, для целей этого вопроса я хочу реализовать свой собственный, используя только SDK (обучение на практике). Учитывая, что кэш будет использоваться в многопоточной среде, какие структуры данных вы бы использовали? Я уже реализовал с помощью LinkedHashMap и коллекции#synchronizedMap, но мне любопытно, будет ли какая-либо из новых параллельных коллекций лучшими кандидатами.
обновление: я был просто читаю Yegge это когда я нашел этот самородок:
Если вам нужен постоянный доступ и вы хотите сохранить порядок вставки, вы не можете сделать лучше, чем LinkedHashMap, действительно замечательная структура данных. Единственный способ, которым это могло бы быть более замечательным, - это если бы была параллельная версия. Но увы.
Я думал почти то же самое, прежде чем я пошел с LinkedHashMap
+ Collections#synchronizedMap
реализация я упомянул выше. Приятно знать, что я ничего не упустила.
основываясь на ответах до сих пор, похоже, что мой лучший выбор для сильно параллельного LRU будет заключаться в расширении ConcurrentHashMap используя некоторые из той же логики, что LinkedHashMap
использует.
20 ответов
мне нравится много этих предложений, но пока я думаю, что я буду придерживаться LinkedHashMap
+ Collections.synchronizedMap
. Если я вернусь к этому в будущем, я, вероятно, буду работать над расширением ConcurrentHashMap
таким же образом LinkedHashMap
выходит HashMap
.
обновление:
по желанию, вот суть моей текущей реализации.
private class LruCache<A, B> extends LinkedHashMap<A, B> {
private final int maxEntries;
public LruCache(final int maxEntries) {
super(maxEntries + 1, 1.0f, true);
this.maxEntries = maxEntries;
}
/**
* Returns <tt>true</tt> if this <code>LruCache</code> has more entries than the maximum specified when it was
* created.
*
* <p>
* This method <em>does not</em> modify the underlying <code>Map</code>; it relies on the implementation of
* <code>LinkedHashMap</code> to do that, but that behavior is documented in the JavaDoc for
* <code>LinkedHashMap</code>.
* </p>
*
* @param eldest
* the <code>Entry</code> in question; this implementation doesn't care what it is, since the
* implementation is only dependent on the size of the cache
* @return <tt>true</tt> if the oldest
* @see java.util.LinkedHashMap#removeEldestEntry(Map.Entry)
*/
@Override
protected boolean removeEldestEntry(final Map.Entry<A, B> eldest) {
return super.size() > maxEntries;
}
}
Map<String, String> example = Collections.synchronizedMap(new LruCache<String, String>(CACHE_SIZE));
Если бы я делал это снова с нуля сегодня, я бы использовал Guava CacheBuilder
.
это второй раунд.
первый раунд был тем, что я придумал, затем я перечитал комментарии с доменом, немного укоренившимся в моей голове.
Итак, вот самая простая версия с модульным тестом, который показывает, что он работает на основе некоторых других версий.
сначала непараллельная версия:
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 без флага true для конструктора будет просто реализовать кэш 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.благо.кэш;
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 и как вы могли бы переключить его с помощью простого перечисления arg, и тогда я понял, что все, чего я действительно хотел, это простой LRU. Поэтому проигнорируйте предыдущее сообщение от меня и дайте мне знать, если вы хотите увидеть кэш LRU/LFU/FIFO, который можно переключить через перечисление... нет? Ладно.. вот он идет.
самый простой 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 ();
}
}
на очереди.removeFirstOccurrence потенциально дорогостоящая операция, если у вас большой кэш. В качестве примера можно взять LinkedList и добавьте хэш-карту обратного поиска из элемента в узел, чтобы сделать операции удаления намного быстрее и согласованнее. Я тоже начал, но потом понял, что мне это не нужно. Но... возможно...
, когда поставить вызывается, ключ добавляется в очередь. Когда get вызывается, ключ удаляется и повторно добавляется в верхнюю часть очереди.
если ваш кэш мал, и создание элемента дорого, то это должно будь хорошим тайником. Если ваш кэш действительно большой, то линейный поиск может быть бутылочным горлышком, особенно если у вас нет горячих областей кэша. Чем интенсивнее горячие точки, тем быстрее линейный поиск, поскольку горячие элементы всегда находятся в верхней части линейного поиска. В любом случае... для этого нужно быстрее написать другой LinkedList, который имеет операцию удаления, которая имеет обратный элемент для поиска узла для удаления, тогда удаление будет примерно так же быстро, как удаление ключа из хэша карта.
если у вас есть кэш под 1000 пунктов, это должно сработать.
вот простой тест, чтобы показать свою работу в действии.
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 и использование блокировки (я мог бы иметь все сошло с рук, но ... ..).
Я не тестировал его под огнем, но это похоже на простой кэш LRU, который может работать в 80% случаев использования, когда вам нужна простая карта LRU.
Я приветствую обратную связь, за исключением того, почему вы не используете библиотеку a, b или C. Причина, по которой я не всегда использую библиотеку, заключается в том, что я не всегда хочу, чтобы каждый файл war был 80MB, и я пишу библиотеки, поэтому я склонен делать библиотеки подключаемыми с достаточно хорошим решением на месте и кем-то можно подключить другого поставщика кэша, если они хотят. :) Я никогда не знаю, когда кому-то может понадобиться Guava или ehcache или что-то еще, я не хочу включать их, но если я сделаю кэширование подключаемым, я не буду исключать их.
уменьшение зависимостей имеет свое вознаграждение. Мне нравится получать отзывы о том, как сделать это еще проще или быстрее или и то, и другое.
и если кто знает готово....
Ok.. Я знаю, о чем ты думаешь... Почему он не просто используйте 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 ();
}
}
FIFOs быстрые. Никаких поисков. Вы можете перед FIFO перед LRU, и это будет обрабатывать большинство горячих записей довольно хорошо. Лучший LRU понадобится этот обратный элемент для функции узла.
в любом случае... теперь, когда я написал код, позвольте мне пройти через другие ответы и посмотреть, что я пропустил... в первый раз я их просмотрел.
LinkedHashMap
является O (1), но требует синхронизации. Нет необходимости изобретать колесо.
2 опции для увеличения параллелизма:
1.
Создать несколько LinkedHashMap
, и хэш в них:
пример: LinkedHashMap[4], index 0, 1, 2, 3
. На ключе do key%4
(или binary OR
on [key, 3]
), чтобы выбрать, какую карту сделать put/get / remove.
2.
Вы могли бы сделать "почти" LRU, расширяя ConcurrentHashMap
и наличие связанной хэш-карты, подобной структуре, в каждой из областей внутри нее. Запирающий будет происходить более детально, чем LinkedHashMap
что синхронизации. На put
или putIfAbsent
требуется только блокировка на голове и хвосте списка (для каждого региона). На Удалить или получить весь регион должен быть заблокирован. Мне любопытно, могут ли атомарные связанные списки помочь здесь - вероятно, так для главы списка. Может быть, для большего.
структура не будет поддерживать общий порядок, а только порядок в регионе. До тех пор, пока количество записей намного больше, чем количество регионов, это достаточно хорошо для большинства тайников. Каждый регион должен будет иметь свой собственный счет входа, это будет использоваться, а не глобальный счет для триггера выселения.
Число регионов по умолчанию в ConcurrentHashMap
это 16, что достаточно для большинства серверов сегодня.
было бы проще писать и быстрее при умеренном параллелизме.
было бы сложнее написать, но масштабировать намного лучше при очень высоком параллелизме. Он будет медленнее для нормального доступа (так же, как
ConcurrentHashMap
медленнее, чемHashMap
где нет параллелизма)
есть две реализации с открытым исходным кодом.
Apache Solr имеет ConcurrentLRUCache: https://lucene.apache.org/solr/3_6_1/org/apache/solr/util/ConcurrentLRUCache.html
существует проект с открытым исходным кодом для ConcurrentLinkedHashMap: http://code.google.com/p/concurrentlinkedhashmap/
Я бы рассмотрел использование java.утиль.параллельный.PriorityBlockingQueue, С приоритетом, определяемым счетчиком "numberOfUses" в каждом элементе. Я был бы очень, очень осторожны чтобы получить всю мою синхронизацию правильно, так как счетчик "numberOfUses" подразумевает, что элемент не может быть неизменяемым.
объект элемента будет оболочкой для объектов в кэше:
class CacheElement {
private final Object obj;
private int numberOfUsers = 0;
CacheElement(Object obj) {
this.obj = obj;
}
... etc.
}
кэш LRU может быть реализован с помощью ConcurrentLinkedQueue и ConcurrentHashMap, которые также могут использоваться в многопоточном сценарии. Глава очереди - это элемент, который находился в очереди самое долгое время. Хвост очереди - это элемент, который находился в очереди самое короткое время. Когда элемент существует на карте, мы можем удалить его из LinkedQueue и вставить его в хвост.
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public class LRUCache<K,V> {
private ConcurrentHashMap<K,V> map;
private ConcurrentLinkedQueue<K> queue;
private final int size;
public LRUCache(int size) {
this.size = size;
map = new ConcurrentHashMap<K,V>(size);
queue = new ConcurrentLinkedQueue<K>();
}
public V get(K key) {
//Recently accessed, hence move it to the tail
queue.remove(key);
queue.add(key);
return map.get(key);
}
public void put(K key, V value) {
//ConcurrentHashMap doesn't allow null key or values
if(key == null || value == null) throw new NullPointerException();
if(map.containsKey(key) {
queue.remove(key);
}
if(queue.size() >= size) {
K lruKey = queue.poll();
if(lruKey != null) {
map.remove(lruKey);
}
}
queue.add(key);
map.put(key,value);
}
}
надеюсь, что это помогает .
import java.util.*;
public class Lru {
public static <K,V> Map<K,V> lruCache(final int maxSize) {
return new LinkedHashMap<K, V>(maxSize*4/3, 0.75f, true) {
private static final long serialVersionUID = -3588047435434569014L;
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
};
}
public static void main(String[] args ) {
Map<Object, Object> lru = Lru.lruCache(2);
lru.put("1", "1");
lru.put("2", "2");
lru.put("3", "3");
System.out.println(lru);
}
}
вот моя реализация для LRU. Я использовал PriorityQueue, который в основном работает как FIFO, а не threadsafe. Используемый компаратор на основе создания времени страницы и на основе выполнения заказа из страниц для наименее недавно использованного времени.
страницы для рассмотрения : 2, 1, 0, 2, 8, 2, 4
страница добавлена в кэш: 2
Страница добавлена в кэш: 1
Страница добавлена в кэш: 0
Страница: 2 уже exisit в кэше. Последнего доступа время обновлено
Ошибка страницы, Страница: 1, заменено на страницу: 8
Страница добавлена в кэш: 8
Страница: 2 уже exisit в кэше. Последнее обновление времени доступа
Ошибка страницы, страница: 0, заменена на страницу: 4
Страница добавлена в кэш: 4
выход
Страницы LRUCache
-------------
Имя Страницы: 8, PageCreationTime: 1365957019974
Страницы: 2, PageCreationTime: 1365957020074
Имя Страницы: 4, PageCreationTime: 1365957020174
введите здесь код
import java.util.Comparator;
import java.util.Iterator;
import java.util.PriorityQueue;
public class LRUForCache {
private PriorityQueue<LRUPage> priorityQueue = new PriorityQueue<LRUPage>(3, new LRUPageComparator());
public static void main(String[] args) throws InterruptedException {
System.out.println(" Pages for consideration : 2, 1, 0, 2, 8, 2, 4");
System.out.println("----------------------------------------------\n");
LRUForCache cache = new LRUForCache();
cache.addPageToQueue(new LRUPage("2"));
Thread.sleep(100);
cache.addPageToQueue(new LRUPage("1"));
Thread.sleep(100);
cache.addPageToQueue(new LRUPage("0"));
Thread.sleep(100);
cache.addPageToQueue(new LRUPage("2"));
Thread.sleep(100);
cache.addPageToQueue(new LRUPage("8"));
Thread.sleep(100);
cache.addPageToQueue(new LRUPage("2"));
Thread.sleep(100);
cache.addPageToQueue(new LRUPage("4"));
Thread.sleep(100);
System.out.println("\nLRUCache Pages");
System.out.println("-------------");
cache.displayPriorityQueue();
}
public synchronized void addPageToQueue(LRUPage page){
boolean pageExists = false;
if(priorityQueue.size() == 3){
Iterator<LRUPage> iterator = priorityQueue.iterator();
while(iterator.hasNext()){
LRUPage next = iterator.next();
if(next.getPageName().equals(page.getPageName())){
/* wanted to just change the time, so that no need to poll and add again.
but elements ordering does not happen, it happens only at the time of adding
to the queue
In case somebody finds it, plz let me know.
*/
//next.setPageCreationTime(page.getPageCreationTime());
priorityQueue.remove(next);
System.out.println("Page: " + page.getPageName() + " already exisit in cache. Last accessed time updated");
pageExists = true;
break;
}
}
if(!pageExists){
// enable it for printing the queue elemnts
//System.out.println(priorityQueue);
LRUPage poll = priorityQueue.poll();
System.out.println("Page Fault, PAGE: " + poll.getPageName()+", Replaced with PAGE: "+page.getPageName());
}
}
if(!pageExists){
System.out.println("Page added into cache is : " + page.getPageName());
}
priorityQueue.add(page);
}
public void displayPriorityQueue(){
Iterator<LRUPage> iterator = priorityQueue.iterator();
while(iterator.hasNext()){
LRUPage next = iterator.next();
System.out.println(next);
}
}
}
class LRUPage{
private String pageName;
private long pageCreationTime;
public LRUPage(String pagename){
this.pageName = pagename;
this.pageCreationTime = System.currentTimeMillis();
}
public String getPageName() {
return pageName;
}
public long getPageCreationTime() {
return pageCreationTime;
}
public void setPageCreationTime(long pageCreationTime) {
this.pageCreationTime = pageCreationTime;
}
@Override
public boolean equals(Object obj) {
LRUPage page = (LRUPage)obj;
if(pageCreationTime == page.pageCreationTime){
return true;
}
return false;
}
@Override
public int hashCode() {
return (int) (31 * pageCreationTime);
}
@Override
public String toString() {
return "PageName: " + pageName +", PageCreationTime: "+pageCreationTime;
}
}
class LRUPageComparator implements Comparator<LRUPage>{
@Override
public int compare(LRUPage o1, LRUPage o2) {
if(o1.getPageCreationTime() > o2.getPageCreationTime()){
return 1;
}
if(o1.getPageCreationTime() < o2.getPageCreationTime()){
return -1;
}
return 0;
}
}
вот моя проверенная лучшая параллельная реализация кэша LRU без какого-либо синхронизированного блока:
public class ConcurrentLRUCache<Key, Value> {
private final int maxSize;
private ConcurrentHashMap<Key, Value> map;
private ConcurrentLinkedQueue<Key> queue;
public ConcurrentLRUCache(final int maxSize) {
this.maxSize = maxSize;
map = new ConcurrentHashMap<Key, Value>(maxSize);
queue = new ConcurrentLinkedQueue<Key>();
}
/**
* @param key - may not be null!
* @param value - may not be null!
*/
public void put(final Key key, final Value value) {
if (map.containsKey(key)) {
queue.remove(key); // remove the key from the FIFO queue
}
while (queue.size() >= maxSize) {
Key oldestKey = queue.poll();
if (null != oldestKey) {
map.remove(oldestKey);
}
}
queue.add(key);
map.put(key, value);
}
/**
* @param key - may not be null!
* @return the value associated to the given key or null
*/
public Value get(final Key key) {
return map.get(key);
}
}
это кэш LRU, который я использую, который инкапсулирует LinkedHashMap и обрабатывает параллелизм с помощью простой блокировки синхронизации, защищающей сочные места. Он "касается" элементов, когда они используются, чтобы они снова стали "самым свежим" элементом, так что на самом деле это LRU. У меня также было требование, чтобы мои элементы имели минимальный срок службы, который вы также можете думать о "максимальном времени простоя", тогда вы можете выселить.
тем не менее, я согласен с выводом Хэнка и принятый ответ - если бы я начал это снова сегодня, я бы проверил Guava CacheBuilder
.
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class MaxIdleLRUCache<KK, VV> {
final static private int IDEAL_MAX_CACHE_ENTRIES = 128;
public interface DeadElementCallback<KK, VV> {
public void notify(KK key, VV element);
}
private Object lock = new Object();
private long minAge;
private HashMap<KK, Item<VV>> cache;
public MaxIdleLRUCache(long minAgeMilliseconds) {
this(minAgeMilliseconds, IDEAL_MAX_CACHE_ENTRIES);
}
public MaxIdleLRUCache(long minAgeMilliseconds, int idealMaxCacheEntries) {
this(minAgeMilliseconds, idealMaxCacheEntries, null);
}
public MaxIdleLRUCache(long minAgeMilliseconds, int idealMaxCacheEntries, final DeadElementCallback<KK, VV> callback) {
this.minAge = minAgeMilliseconds;
this.cache = new LinkedHashMap<KK, Item<VV>>(IDEAL_MAX_CACHE_ENTRIES + 1, .75F, true) {
private static final long serialVersionUID = 1L;
// This method is called just after a new entry has been added
public boolean removeEldestEntry(Map.Entry<KK, Item<VV>> eldest) {
// let's see if the oldest entry is old enough to be deleted. We don't actually care about the cache size.
long age = System.currentTimeMillis() - eldest.getValue().birth;
if (age > MaxIdleLRUCache.this.minAge) {
if ( callback != null ) {
callback.notify(eldest.getKey(), eldest.getValue().payload);
}
return true; // remove it
}
return false; // don't remove this element
}
};
}
public void put(KK key, VV value) {
synchronized ( lock ) {
// System.out.println("put->"+key+","+value);
cache.put(key, new Item<VV>(value));
}
}
public VV get(KK key) {
synchronized ( lock ) {
// System.out.println("get->"+key);
Item<VV> item = getItem(key);
return item == null ? null : item.payload;
}
}
public VV remove(String key) {
synchronized ( lock ) {
// System.out.println("remove->"+key);
Item<VV> item = cache.remove(key);
if ( item != null ) {
return item.payload;
} else {
return null;
}
}
}
public int size() {
synchronized ( lock ) {
return cache.size();
}
}
private Item<VV> getItem(KK key) {
Item<VV> item = cache.get(key);
if (item == null) {
return null;
}
item.touch(); // idle the item to reset the timeout threshold
return item;
}
private static class Item<T> {
long birth;
T payload;
Item(T payload) {
this.birth = System.currentTimeMillis();
this.payload = payload;
}
public void touch() {
this.birth = System.currentTimeMillis();
}
}
}
Ну для кэша вы обычно будете искать некоторую часть данных через прокси-объект (URL, String....) таким образом, интерфейс мудрый вы будете хотеть карту. но для начала необходимо очередь как структура. Внутренне я бы поддерживал две структуры данных, приоритетную очередь и хэш-карту. вот реализация, которая должна быть в состоянии сделать все за O(1) Время.
вот класс, который я взбитые довольно быстро:
import java.util.HashMap;
import java.util.Map;
public class LRUCache<K, V>
{
int maxSize;
int currentSize = 0;
Map<K, ValueHolder<K, V>> map;
LinkedList<K> queue;
public LRUCache(int maxSize)
{
this.maxSize = maxSize;
map = new HashMap<K, ValueHolder<K, V>>();
queue = new LinkedList<K>();
}
private void freeSpace()
{
K k = queue.remove();
map.remove(k);
currentSize--;
}
public void put(K key, V val)
{
while(currentSize >= maxSize)
{
freeSpace();
}
if(map.containsKey(key))
{//just heat up that item
get(key);
return;
}
ListNode<K> ln = queue.add(key);
ValueHolder<K, V> rv = new ValueHolder<K, V>(val, ln);
map.put(key, rv);
currentSize++;
}
public V get(K key)
{
ValueHolder<K, V> rv = map.get(key);
if(rv == null) return null;
queue.remove(rv.queueLocation);
rv.queueLocation = queue.add(key);//this ensures that each item has only one copy of the key in the queue
return rv.value;
}
}
class ListNode<K>
{
ListNode<K> prev;
ListNode<K> next;
K value;
public ListNode(K v)
{
value = v;
prev = null;
next = null;
}
}
class ValueHolder<K,V>
{
V value;
ListNode<K> queueLocation;
public ValueHolder(V value, ListNode<K> ql)
{
this.value = value;
this.queueLocation = ql;
}
}
class LinkedList<K>
{
ListNode<K> head = null;
ListNode<K> tail = null;
public ListNode<K> add(K v)
{
if(head == null)
{
assert(tail == null);
head = tail = new ListNode<K>(v);
}
else
{
tail.next = new ListNode<K>(v);
tail.next.prev = tail;
tail = tail.next;
if(tail.prev == null)
{
tail.prev = head;
head.next = tail;
}
}
return tail;
}
public K remove()
{
if(head == null)
return null;
K val = head.value;
if(head.next == null)
{
head = null;
tail = null;
}
else
{
head = head.next;
head.prev = null;
}
return val;
}
public void remove(ListNode<K> ln)
{
ListNode<K> prev = ln.prev;
ListNode<K> next = ln.next;
if(prev == null)
{
head = next;
}
else
{
prev.next = next;
}
if(next == null)
{
tail = prev;
}
else
{
next.prev = prev;
}
}
}
вот как это работает. Ключи хранится в связанном списке с самыми старыми ключами в передней части списка (новые ключи идут назад), поэтому, когда вам нужно "извлечь" что-то, вы просто вытащите его из передней части очереди, а затем используйте ключ, чтобы удалить значение с карты. Когда элемент получает ссылку, вы берете ValueHolder с карты, а затем используете переменную queuelocation для удаления ключа из его текущего местоположения в очереди, а затем помещаете его в конец очереди (теперь он используется последний раз). Добавление вещей, довольно так же.
Я уверен, что здесь тонна ошибок, и я не реализовал никакой синхронизации. но этот класс обеспечит O(1) Добавление в кэш, O(1) Удаление старых элементов и O (1) извлечение элементов кэша. Даже тривиальная синхронизация (просто синхронизируйте каждый общедоступный метод) все равно будет иметь мало конфликтов блокировки из-за времени выполнения. Если у кого-то есть умные трюки синхронизации, мне было бы очень интересно. Кроме того, я уверен, что есть некоторые дополнительные оптимизации это можно реализовать с помощью переменной maxsize относительно карты.
посмотреть ConcurrentSkipListMap. Это должно дать вам log (n) время для тестирования и удаления элемента, если он уже содержится в кэше, и постоянное время для его повторного добавления.
вам просто нужен какой-то счетчик и т. д. и элемент обертки, чтобы принудительно упорядочить порядок LRU и гарантировать, что последние вещи отбрасываются, когда кэш заполнен.
вот моя короткая реализация, пожалуйста, критикуйте или улучшайте ее!
package util.collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Limited size concurrent cache map implementation.<br/>
* LRU: Least Recently Used.<br/>
* If you add a new key-value pair to this cache after the maximum size has been exceeded,
* the oldest key-value pair will be removed before adding.
*/
public class ConcurrentLRUCache<Key, Value> {
private final int maxSize;
private int currentSize = 0;
private ConcurrentHashMap<Key, Value> map;
private ConcurrentLinkedQueue<Key> queue;
public ConcurrentLRUCache(final int maxSize) {
this.maxSize = maxSize;
map = new ConcurrentHashMap<Key, Value>(maxSize);
queue = new ConcurrentLinkedQueue<Key>();
}
private synchronized void freeSpace() {
Key key = queue.poll();
if (null != key) {
map.remove(key);
currentSize = map.size();
}
}
public void put(Key key, Value val) {
if (map.containsKey(key)) {// just heat up that item
put(key, val);
return;
}
while (currentSize >= maxSize) {
freeSpace();
}
synchronized(this) {
queue.add(key);
map.put(key, val);
currentSize++;
}
}
public Value get(Key key) {
return map.get(key);
}
}
вот моя собственная реализация этой проблемы
simplelrucache обеспечивает threadsafe, очень простое, не распределенное кэширование LRU с поддержкой TTL. Он предоставляет две реализации:
- Concurrent на основе ConcurrentLinkedHashMap
- синхронизировано на основе LinkedHashMap
вы можете найти его здесь: http://code.google.com/p/simplelrucache/
Я ищу лучший кэш LRU, используя Java-код. Возможно ли вам поделиться своим кодом кэша Java LRU с помощью LinkedHashMap
и Collections#synchronizedMap
? В настоящее время я использую LRUMap implements Map
и код работает нормально, но я получаю ArrayIndexOutofBoundException
на нагрузочном тестировании используя 500 потребителей на внизу методе. Метод перемещает объект recent в начало очереди.
private void moveToFront(int index) {
if (listHead != index) {
int thisNext = nextElement[index];
int thisPrev = prevElement[index];
nextElement[thisPrev] = thisNext;
if (thisNext >= 0) {
prevElement[thisNext] = thisPrev;
} else {
listTail = thisPrev;
}
//old listHead and new listHead say new is 1 and old was 0 then prev[1]= 1 is the head now so no previ so -1
// prev[0 old head] = new head right ; next[new head] = old head
prevElement[index] = -1;
nextElement[index] = listHead;
prevElement[listHead] = index;
listHead = index;
}
}
get(Object key)
и put(Object key, Object value)
метод вызывает вышеуказанное moveToFront
метод.
хотел добавить комментарий к ответу, данному Хэнком, но некоторые, как я не могу-пожалуйста, рассматривайте его как комментарий
LinkedHashMap также поддерживает порядок доступа на основе параметра, переданного в его конструкторе Он сохраняет дважды выровненный список для поддержания порядка (см. LinkedHashMap.Запись)
@Pacerier правильно, что LinkedHashMap сохраняет тот же порядок во время итерации, если элемент добавляется снова, но это только в случае режима порядка вставки.
это что я нашел в java-документах LinkedHashMap.Объект входа
/**
* This method is invoked by the superclass whenever the value
* of a pre-existing entry is read by Map.get or modified by Map.set.
* If the enclosing Map is access-ordered, it moves the entry
* to the end of the list; otherwise, it does nothing.
*/
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
этот метод заботится о перемещении недавно доступного элемента в конец списка. Таким образом, в целом LinkedHashMap-лучшая структура данных для реализации LRUCache.
еще одна мысль и даже простая реализация с использованием LinkedHashMap коллекции Java.
LinkedHashMap предоставил метод removeEldestEntry и который можно переопределить способом, упомянутым в Примере. По умолчанию реализация этой структуры коллекции имеет значение false. Если его истинный и размер этой структуры выходит за пределы начальной емкости, то будут удалены самые старые или более старые элементы.
мы можем иметь pageno и содержимое страницы в моем случае pageno является целочисленным и pagecontent я сохранил строку значений номера страницы.
import java.util.LinkedHashMap; import java.util.Map; /** * @author Deepak Singhvi * */ public class LRUCacheUsingLinkedHashMap { private static int CACHE_SIZE = 3; public static void main(String[] args) { System.out.println(" Pages for consideration : 2, 1, 0, 2, 8, 2, 4,99"); System.out.println("----------------------------------------------\n"); // accessOrder is true, so whenever any page gets changed or accessed, // its order will change in the map, LinkedHashMap<Integer,String> lruCache = new LinkedHashMap<Integer,String>(CACHE_SIZE, .75F, true) { private static final long serialVersionUID = 1L; protected boolean removeEldestEntry(Map.Entry<Integer,String> eldest) { return size() > CACHE_SIZE; } }; lruCache.put(2, "2"); lruCache.put(1, "1"); lruCache.put(0, "0"); System.out.println(lruCache + " , After first 3 pages in cache"); lruCache.put(2, "2"); System.out.println(lruCache + " , Page 2 became the latest page in the cache"); lruCache.put(8, "8"); System.out.println(lruCache + " , Adding page 8, which removes eldest element 2 "); lruCache.put(2, "2"); System.out.println(lruCache+ " , Page 2 became the latest page in the cache"); lruCache.put(4, "4"); System.out.println(lruCache+ " , Adding page 4, which removes eldest element 1 "); lruCache.put(99, "99"); System.out.println(lruCache + " , Adding page 99, which removes eldest element 8 "); } }
результат выполнения вышеуказанного кода выглядит следующим образом:
Pages for consideration : 2, 1, 0, 2, 8, 2, 4,99
--------------------------------------------------
{2=2, 1=1, 0=0} , After first 3 pages in cache
{2=2, 1=1, 0=0} , Page 2 became the latest page in the cache
{1=1, 0=0, 8=8} , Adding page 8, which removes eldest element 2
{0=0, 8=8, 2=2} , Page 2 became the latest page in the cache
{8=8, 2=2, 4=4} , Adding page 4, which removes eldest element 1
{2=2, 4=4, 99=99} , Adding page 99, which removes eldest element 8