Пожалуйста, не говорите EHCache или OSCache и т. Д. Предположим, что для целей этого вопроса я хочу реализовать свой собственный, используя только SDK (обучение на практике). Учитывая, что кеш будет использоваться в многопоточной среде, какие структуры данных вы бы использовали? Я уже реализовал один, используя LinkedHashMap и Коллекции # synchronizedMap , но мне любопытно, будут ли какие-либо из новых параллельных коллекций более подходящими кандидатами.
ОБНОВЛЕНИЕ: я только что прочитал последние Йегге, когда я нашел этот самородок:
Если вам нужен постоянный доступ и вы хотите поддерживать порядок вставки, вы не можете сделать лучше, чем LinkedHashMap, действительно замечательная структура данных. Единственный способ, которым это могло бы быть более чудесным, - это наличие параллельной версии. Но увы.
Я думал почти то же самое, прежде чем я пошел с LinkedHashMap
+Collections#synchronizedMap
реализации я упоминал выше. Приятно осознавать, что я что-то не заметил.
Исходя из полученных ответов, мне кажется, что лучшим вариантом для LRU с высокой степенью параллелизма было бы расширение ConcurrentHashMap с использованием той же логики, которая LinkedHashMap
используется.
источник
O(1)
требуемая версия: stackoverflow.com/questions/23772102/…Ответы:
Мне нравятся многие из этих предложений, но сейчас я думаю, что буду придерживаться
LinkedHashMap
+Collections.synchronizedMap
. Если я еще вернусь к этому в будущем, я, вероятно, буду работать над расширениемConcurrentHashMap
таким же образом, какLinkedHashMap
расширяетHashMap
.ОБНОВИТЬ:
По запросу, вот суть моей текущей реализации.
источник
LinkedHashMap
явно одобрил этот метод для создания реализации LRU.Если бы я делал это снова с нуля сегодня, я бы использовал Guava
CacheBuilder
.источник
Это второй раунд.
Первый раунд был то, что я придумал, затем я перечитал комментарии с доменом, немного более укоренившимся в моей голове.
Итак, вот самая простая версия с модульным тестом, которая показывает, что она работает на основе некоторых других версий.
Сначала не параллельная версия:
Истинный флаг будет отслеживать доступ получает и кладет. Смотрите JavaDocs. RemoveEdelstEntry без флага true для конструктора просто реализует кеш FIFO (см. Примечания ниже по FIFO и removeEldestEntry).
Вот тест, который доказывает, что он работает как кэш LRU:
Теперь для параллельной версии ...
пакет org.boon.cache;
Вы можете понять, почему я сначала расскажу о несовместимой версии. Вышеприведенное пытается создать несколько полос для уменьшения конкуренции за блокировку. Таким образом, мы хэшируем ключ и затем ищем этот хеш, чтобы найти фактический кеш. Это делает предельный размер скорее предположением / грубым предположением при значительном количестве ошибок, в зависимости от того, насколько хорошо распределен ваш алгоритм хеширования ключей.
Вот тест, чтобы показать, что параллельная версия, вероятно, работает. :) (Испытание под огнем было бы реальным способом).
Это последний пост. Первый пост, который я удалил, так как это был LFU, а не LRU-кеш.
Я думал, что я сделаю это еще раз. Я пытался найти простейшую версию LRU-кеша, используя стандартную JDK без слишком большой реализации.
Вот что я придумал. Моя первая попытка была немного неудачной, так как я внедрил LFU вместо LRU, а затем я добавил FIFO и поддержку LRU ... и затем я понял, что он становится монстром. Затем я начал разговаривать со своим приятелем Джоном, который едва интересовался, а затем я подробно описал, как я реализовал LFU, LRU и FIFO и как вы можете переключать его с помощью простого аргумента ENUM, и затем я понял, что все, чего я действительно хотел был простой LRU. Так что проигнорируйте мой предыдущий пост и дайте мне знать, если вы хотите увидеть кэш LRU / LFU / FIFO, который можно переключать с помощью enum ... нет? Хорошо .. вот и он.
Простейший из возможных LRU, использующий только JDK. Я реализовал как параллельную версию, так и не параллельную версию.
Я создал общий интерфейс (это минимализм, поэтому, скорее всего, отсутствуют некоторые функции, которые вы хотели бы, но он работает для моих случаев использования, но позвольте, если вы хотите увидеть функцию XYZ, дайте мне знать ... Я живу, чтобы писать код.) ,
Вы можете спросить, что такое getSilent . Я использую это для тестирования. getSilent не изменяет оценку LRU элемента.
Сначала не совпадающий ....
Queue.removeFirstOccurrence является потенциально дорогостоящей операцией , если у вас есть большой кэш. Можно взять LinkedList в качестве примера и добавить хэш-карту обратного просмотра от элемента к узлу, чтобы сделать операции удаления ОЧЕНЬ БЫСТРЕЕ и более согласованными. Я тоже начал, но потом понял, что мне это не нужно. Но возможно...
Когда вызывается метод put , ключ добавляется в очередь. Когда получить вызывается , ключ удаляется и снова добавляется в начало очереди.
Если у вас маленький кеш, а создание предмета стоит дорого, то это должен быть хороший кеш. Если ваш кеш очень большой, то линейный поиск может быть узким местом, особенно если у вас нет горячих областей кеша. Чем интенсивнее горячие точки, тем быстрее линейный поиск, поскольку горячие объекты всегда находятся на вершине линейного поиска. В любом случае ... для ускорения процесса необходимо написать еще один LinkedList, в котором есть операция удаления, которая имеет обратный элемент для поиска узлов для удаления, тогда удаление будет примерно таким же быстрым, как удаление ключа из хэш-карты.
Если у вас есть кеш менее 1000 элементов, это должно работать нормально.
Вот простой тест, чтобы показать его действия в действии.
Последний LRU-кэш был однопоточным, и, пожалуйста, не включайте его в синхронизированный файл ...
Вот удар по параллельной версии.
Основными отличиями являются использование ConcurrentHashMap вместо HashMap и использование блокировки (я мог бы обойтись без синхронизации, но ...).
Я не тестировал его под огнем, но он выглядит как простой кэш LRU, который может сработать в 80% случаев, когда вам нужна простая карта LRU.
Я приветствую обратную связь, за исключением того, почему вы не используете библиотеку a, b или c. Причина, по которой я не всегда использую библиотеку, заключается в том, что я не всегда хочу, чтобы каждый war-файл занимал 80 МБ, и я пишу библиотеки, поэтому я стремлюсь сделать библиотеки подключаемыми с достаточно хорошим решением, и кто-то может подключить -в другом провайдере кэша, если им нравится. :) Я никогда не знаю, когда кому-то может понадобиться Guava или ehcache или что-то еще, я не хочу их включать, но если я сделаю кэширование подключаемым, я тоже не буду исключать их.
Сокращение зависимостей имеет свою награду. Мне нравится получать отзывы о том, как сделать это еще проще, быстрее или и то, и другое.
Также, если кто-нибудь знает о готовом к работе ....
Хорошо ... Я знаю, о чем вы думаете ... Почему он просто не использует запись removeEldest из LinkedHashMap, и хорошо, но я должен, но .... но ... но .. Это было бы FIFO, а не LRU, и мы были пытаясь реализовать LRU.
Этот тест не проходит для приведенного выше кода ...
Итак, вот быстрый и грязный FIFO-кеш с использованием removeEldestEntry.
FIFO быстрые. Нет поиска вокруг. Вы могли бы выставить FIFO перед LRU, и это очень хорошо обработало бы самые горячие записи. Лучшему LRU понадобится этот обратный элемент для функции Node.
Во всяком случае ... теперь, когда я написал некоторый код, позвольте мне пройтись по другим ответам и посмотреть, что я пропустил ... при первом сканировании.
источник
LinkedHashMap
является O (1), но требует синхронизации. Не нужно изобретать велосипед там.2 варианта увеличения параллелизма:
1. Создайте несколько
LinkedHashMap
, и хэш в них: пример:LinkedHashMap[4], index 0, 1, 2, 3
. На клавише dokey%4
(илиbinary OR
on[key, 3]
) выберите карту для установки / получения / удаления.2. Вы могли бы создать «почти» LRU, расширив его
ConcurrentHashMap
и добавив структуру хеш-карты, подобную структуре, в каждой из областей внутри него. Блокировка будет происходить более детально, чемLinkedHashMap
синхронизация. Наput
илиputIfAbsent
только блокировка головы и хвоста списка необходима (для региона). При удалении или получении весь регион должен быть заблокирован. Мне любопытно, могут ли здесь помочь какие-то списки связанных списков Atomic, вероятно, для главы списка. Возможно для большего.Структура будет хранить не общий заказ, а только заказ на регион. Пока количество записей намного больше, чем количество регионов, этого достаточно для большинства кэшей. Каждый регион должен иметь свой собственный счетчик записей, который будет использоваться вместо глобального счета для триггера выселения. Число областей по умолчанию в a
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/
источник
ConcurrentLinkedHashMap
это интересно. Он утверждает, что был свернут вMapMaker
из Гуавы, но я не нашел его в документах. Есть идеи, что происходит с этими усилиями?Я хотел бы рассмотреть возможность использования java.util.concurrent.PriorityBlockingQueue с приоритетом, определяемым счетчиком «numberOfUses» в каждом элементе. Я был бы очень, очень осторожен, чтобы все мои синхронизации были правильными, поскольку счетчик «numberOfUses» подразумевает, что элемент не может быть неизменным.
Элемент object будет оболочкой для объектов в кеше:
источник
Надеюсь это поможет .
источник
Кэш LRU может быть реализован с использованием ConcurrentLinkedQueue и ConcurrentHashMap, которые также могут использоваться в сценарии многопоточности. Голова очереди - это тот элемент, который находился в очереди дольше всего. Хвост очереди - это тот элемент, который находился в очереди кратчайшее время. Когда элемент существует на карте, мы можем удалить его из LinkedQueue и вставить его в хвост.
источник
put
.Вот моя реализация для LRU. Я использовал PriorityQueue, который в основном работает как FIFO, а не как потокобезопасный. Используемый компаратор на основе создания времени страницы и на основе выполняет упорядочивание страниц за наименее использованное время.
Страницы для рассмотрения: 2, 1, 0, 2, 8, 2, 4
Страница добавлена в кеш: 2
Страница добавлена в кеш: 1
Страница добавлена в кеш: 0
Страница: 2 уже существует в кеше. Время последнего доступа обновлено.
Ошибка страницы, PAGE: 1, заменена на PAGE: 8
Страница, добавленная в кэш: 8
Страница: 2 уже существует в кеше. Время последнего обращения к обновленной
странице Ошибка, PAGE: 0, заменено на PAGE: 4
Страница, добавленная в кеш: 4
ВЫВОД
LRUCache Pages
-------------
PageName: 8, PageCreationTime: 1365957019974
PageName: 2, PageCreationTime: 1365957020074
PageName: 4, PageCreationTime: 1365957020174
введите код сюда
источник
Вот моя проверенная наиболее эффективная параллельная реализация кэша LRU без какого-либо синхронизированного блока:
}
источник
Это кеш LRU, который я использую, который инкапсулирует LinkedHashMap и обрабатывает параллелизм с помощью простой блокировки синхронизации, защищающей сочные участки. Он «касается» элементов по мере их использования, чтобы они снова стали «самыми свежими» элементами, так что это фактически LRU. У меня также было требование, чтобы у моих элементов была минимальная продолжительность жизни, которую вы также можете считать «максимальным временем простоя», тогда вы готовы к выселению.
Тем не менее, я согласен с выводом Хэнка и принятым ответом - если бы я начал это сегодня снова, я бы проверил мнение Гуавы
CacheBuilder
.источник
Что касается кеша, вы, как правило, будете искать часть данных через прокси-объект (URL, String ....), поэтому для интерфейса вам понадобится карта. но чтобы выкинуть вещи, вам нужна очередь, такая как структура. Внутренне я бы поддерживал две структуры данных, Priority-Queue и HashMap. Вот реализация, которая должна быть в состоянии сделать все за O (1) времени.
Вот класс, который я довольно быстро взбил:
Вот как это работает. Ключи хранятся в связанном списке с самыми старыми ключами в начале списка (новые ключи идут назад), поэтому, когда вам нужно «извлечь» что-то, вы просто выталкиваете его из передней части очереди, а затем используете клавишу для удалить значение с карты. Когда на элемент ссылаются, вы берете ValueHolder с карты, а затем используете переменную queuelocation, чтобы удалить ключ из его текущего местоположения в очереди, а затем поместить его в конец очереди (теперь он используется самым последним). Добавление вещей - это почти то же самое.
Я уверен, что здесь куча ошибок, и я не реализовал никакой синхронизации. но этот класс обеспечит O (1) добавление в кеш, O (1) удаление старых элементов и O (1) извлечение элементов кеша. Даже обычная синхронизация (просто синхронизировать каждый публичный метод) все равно будет иметь небольшую конкуренцию за блокировку из-за времени выполнения. Если у кого-нибудь есть какие-нибудь хитрые приемы синхронизации, мне было бы очень интересно. Кроме того, я уверен, что есть некоторые дополнительные оптимизации, которые вы могли бы реализовать, используя переменную maxsize относительно карты.
источник
LinkedHashMap
+Collections.synchronizedMap()
?Посмотрите на ConcurrentSkipListMap . Это должно дать вам log (n) время для тестирования и удаления элемента, если он уже содержится в кэше, и постоянное время для его повторного добавления.
Вам просто понадобится некоторый счетчик и т. Д. И элемент-обертка, чтобы принудительно упорядочить порядок LRU и убедиться, что последние данные отбрасываются, когда кэш заполнен.
источник
ConcurrentSkipListMap
дать преимущество в простоте реализацииConcurrentHashMap
или это просто случай избежать патологических случаев?ConcurrentSkipListMap
реализацией, я бы создал новую реализациюMap
интерфейса, который делегируетConcurrentSkipListMap
и выполняет какое-то обертывание, чтобы произвольные типы ключей были обернуты в тип, который легко сортируется на основе последнего доступа?Вот моя короткая реализация, пожалуйста, критикуйте или улучшайте ее!
источник
Вот моя собственная реализация этой проблемы
simplelrucache обеспечивает потоковое, очень простое, нераспределенное LRU-кэширование с поддержкой TTL Он обеспечивает две реализации:
Вы можете найти его здесь: http://code.google.com/p/simplelrucache/
источник
Лучший способ добиться этого - использовать LinkedHashMap, который поддерживает порядок вставки элементов. Ниже приведен пример кода:
}
источник
Я ищу лучший кэш LRU с использованием кода Java. Можно ли поделиться кодом кеша Java LRU с помощью
LinkedHashMap
иCollections#synchronizedMap
? В настоящее время я использую,LRUMap implements Map
и код работает нормально, но яArrayIndexOutofBoundException
использую нагрузочное тестирование с использованием 500 пользователей по приведенному ниже методу. Метод перемещает последний объект в начало очереди.get(Object key)
иput(Object key, Object value)
метод вызывает вышеуказанныйmoveToFront
метод.источник
Хотел добавить комментарий к ответу, данному Хэнком, но кое-что, как я не могу - пожалуйста, относитесь к нему как к комментарию
LinkedHashMap также поддерживает порядок доступа на основе параметра, переданного в его конструкторе. Он поддерживает двунаправленный список для поддержания порядка (см. LinkedHashMap.Entry).
@Pacerier это правильно, что LinkedHashMap сохраняет тот же порядок во время итерации, если элемент добавляется снова, но это только в случае режима порядка вставки.
это то, что я нашел в документации по Java объекта LinkedHashMap.Entry
этот метод заботится о перемещении недавно использованного элемента в конец списка. В общем, LinkedHashMap - лучшая структура данных для реализации LRUCache.
источник
Еще одна мысль и даже простая реализация, использующая коллекцию Java LinkedHashMap.
LinkedHashMap предоставляет метод removeEldestEntry, который может быть переопределен способом, упомянутым в примере. По умолчанию реализация этой структуры коллекции ложна. Если его истинность и размер этой структуры выходят за пределы первоначальной емкости, то старые или более старые элементы будут удалены.
У нас может быть pageno и содержимое страницы, в моем случае pageno является целым числом и содержанием страницы. Я сохранил строку значений номера страницы.
Результат выполнения вышеуказанного кода выглядит следующим образом:
источник
Следуя концепции @sanjanab (но после исправлений), я сделал свою версию LRUCache, предоставив также Consumer, который позволяет делать что-то с удаленными элементами при необходимости.
источник
Android предлагает реализацию LRU Cache . Код чист и прост.
источник