vlambda博客
学习文章列表

具有LRU回收策略的Java缓存


    点击“蓝字”关注我们获得资讯/干货/内推
介绍

LRU(Least Recently Used)是一种高速缓存清除策略,其中如果缓存大小已达到最大分配容量,则将清除缓存中最近最少访问的对象。此外,缓存中的对象可以被应用程序中的多个线程访问,因此,缓存具有良好的内置同步机制非常重要。

本文描述了基于Java的LRU回收策略的实现;但基本上适用于任何编程语言。


背景

很多时候,开发人员会在他们的应用程序中嵌入缓存框架,比如Ehcache(这是一个用于一般用途的、内存缓存的很棒的库)。但是,当没有自由使用这些框架或库时,就会出现问题。

例如,轻量级Java应用程序或移动应用程序的目标是在最小的内存占用上运行(或者由于交付期限的原因,有时没有额外的时间来学习和配置第三方缓存框架并将其部署到生产环境中)。

在这种情况下,可以使用这样一个简单、轻量级的LRUCache类,并使用足够的单元测试覆盖率,因此可以大量的依赖缓存。


本文解释了如何使用ConcurrentHashMap和一个双链表数据结构来使用LRU策略实现高速缓存。使用LinkedHashMap也可以达到同样的效果,但是默认情况下它不是线程安全的(也许在下一篇文章中,我将使用LinkedHashMap提交一个)。


设计

在设计基于LRU的高速缓存之前,我们需要了解高速缓存的一些重要特性:
1.应该按O(1)的顺序为get操作提供最小延迟
2.支持并发读/写访问
3.缓存限制应该设置为固定大小(根据对象计数)
4.向缓存中插入新对象应该总是成功的,并且将由键(惟一的)标识
5.对象清除策略应该预先定义,在这种情况下是LRU!


考虑到以上5个属性,可以使用Java的ConcurrentHashMap(满足属性1-4)和简单的double - linked - list(满足属性5)实现一个简单的LRU缓存。


使用ConcurrentHashMap——保存对象并通过键访问它们,以便访问时间为O(1)顺序,提供并发访问,可以设置大小限制,并且可以使用键访问对象。


一个双链表(DLL)维护指向相同对象的指针(在映射中)。这个DLL帮助我们实现LRU回收策略,其中频繁访问的对象位于尾端,访问最少的对象位于列表的开头。


当一个新对象被插入到缓存中时,如果缓存已经达到最大容量,就会触发LRU清除。基本上在DLL中,头节点(最近最少使用的对象)将被删除,同样的也将从映射中删除。


流程图

添加对象到缓存put(K k, V v)


具有LRU回收策略的Java缓存


从缓存获取对象get(K k)


具有LRU回收策略的Java缓存


使用代码

LRU缓存的实现是通过实现类LRUCache<K, V>来实现的。这个缓存对象由一个键、K ——唯一可识别的参数(首选不可变对象)和V -值来标识,V——值可以是任何类型的自定义对象。如果为另一个对象重复一个键,那么缓存将用新对象替换旧对象。

对象实例化:

// 1. Create a new LRUCache instance by passing the initial storage capacity (10 in below example)
LRUCache<String, String> cache = new LRUCache<String, String>(10);


使用put(K k, V v)方法将对象插入到缓存中:

// inserts 10 objects to cache
for(int i=1; i<=10; i++) {
cache.put(String.format("key-%d", i), String.format("value-%d", i));
}


使用printCache()方法打印缓存中的对象的实用方法(将按最近访问的对象与最近访问的对象的顺序打印)

// print the cache objects
cache.printCache();


使用相应的密钥获取存储在缓存中的对象的方法 get(K k)

 
   
   
 
// access the first object and print the cache
cache.get("key-1");
// printing cache after accessing key-1
cache.printCache();


改进之处:

LRUCache类是非常基础的,还缺少其他功能,比如从缓存中删除对象。

如果LRUCache类实现了一个缓存接口,这将是一个好主意,这样就可以使用不同的回收策略实现多个缓存。其他一些可以考虑的回收可能是:
1.先进先出(FIFO)
2.随机LRU
3.排序的(根据某种排序顺序)


兴趣点

请阅读Java的ConcurrentHashMap——如果您在一个多线程环境中运行,那么它的构建是为了执行,并且与普通的HashMap相比具有出色的功能。

有多种缓存清除策略,比如最近使用(MRU)、计数分钟草图、生存时间等等,所有这些都有特定的用例。LRU是最常用的回收策略,解决了我们的大部分需求。


高速缓存框架和分布式缓存的使用在过去的几年里由于高数据量的应用而增长。当内存有限时,应用这些回收策略的挑战变得非常有趣,存储的对象分布在多个节点上,而回收必须应用在这个分布中,保存大型对象和二进制数据等等。


REDIS和Memcache是两个众所周知的分布式缓存服务器。REDIS提供了比普通缓存多一点的功能,它是一个数据结构缓存,具有磁盘持久性选项。


在当前的实现中,使用一个简单的类来实现基于LRU的缓存和使用键值对存储对象。


在此实现的下一个迭代中,计划构建一个简单的缓存库,其中包含各种功能,如添加POJO对象、支持Set接口等。添加工厂模式来创建各种缓存并外部化缓存策略(如LRU、MRU、基于时间的等)。


完整代码
 
   
   
 
package io.howto;import java.util.concurrent.ConcurrentHashMap;
/**
*
* LRUCache: Class to cache objects based on LRU (Least Recently Used) cache eviction strategy,
* wherein if the cache size has reached the maximum allocated capacity, the
* least recently used objects in the cache will be evicted.
*
* See the runner function(a.k.a main(...)) to see the working.
*
* @author sunil
*
* @param <K>* @param <V>
*
* <p>
* Date: 14/Nov/2019
* </p>
*/
public final class LRUCache <K,V> {
/**
* A doubly-linked-list implementation to save objects into the hashmap
* as key-value pari.
*
* @author sunil
*
* @param <K>
* @param <V>*/
private static class Node<K,V> {
private V value;
private K key;
private Node<K, V> next, prev;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return value.toString();
}}
/**
* The maximum number of elements that can be cached, should be set during instantiation
* time.
*/
private finalintmaxCapacity;
/**
* Use {@linkplain ConcurrentHashMap} here to maintain the cache of objects.
* Also this offers thread safe access of the cache.
*/
private ConcurrentHashMap<K, Node<K, V>> map;
/**
* A key-value representation of the cache object identified by a cache key.
* This is actually a doubly-linked list which maintains the most recently accessed
* objects (read/write) at the tail-end and the least read objects at the head.
*/
private Node<K, V> head, tail;
public LRUCache(intmaxCapacity) {
this(16, maxCapacity);
}
public LRUCache(intinitialCapacity, intmaxCapacity) {
this.maxCapacity = maxCapacity;
if (initialCapacity > maxCapacity)
initialCapacity = maxCapacity;
map = newConcurrentHashMap<>(initialCapacity);
}/**
* Removes a node from the head position doubly-linked list.
* @param node
*/
privatevoidremoveNode(Node<K, V> node) {
if(node == null)
return;
if(node.prev != null) {
node.prev.next = node.next;
} else {
head = node.next;
}
if (node.next != null) {
node.next.prev = node.prev;} else {
tail = node.prev;
}
}
/**
* Offers a node to the tail-end of the doubly-linked list because
* it was recently read or written.
* @param node
*/
private void offerNode(Node<K, V> node) {
if (node == null)
return;
if (head == null) {head = tail = node;
} else {
tail.next = node;
node.prev = tail;
node.next = null;
tail = node;
}
}
/**
* Adds a new object to the cache. If the cache size has reached it's capacity,
* then the least recently accessed object will be evicted.
*
* @param key* @param value
*/
public void put(K key, V value) {
if (map.contains(key)) {
Node<K, V> node = map.get(key);
node.value = value;
removeNode(node);
offerNode(node);
} else {
if (map.size() == maxCapacity) {
System.out.println("maxCapacity of cache reached");
map.remove(head.key);
removeNode(head);}
Node<K, V> node = new Node<K, V>(key, value);
offerNode(node);
map.put(key, node);
}
}
/**
* Fetches an object from the cache (could be null if no such mapping exists).
* If the object is found in the cache, then it will be moved to the tail-end of the
* doubly-linked list to indicate that it was recently accessed.
*
* @param key
* @param value
*/public V get(K key) {
Node<K, V> node = map.get(key);
removeNode(node);
offerNode(node);
return node != null ? node.value : null;
}
/**
* Utility function to print the cache objects.
*/
public void printCache() {
Node<K, V> curr = head;
while (curr != null) {
System.out.print(curr.value + " -> ");curr = curr.next;
}
System.out.println();
}
/**
* Runner program to test the LRU cache
* @param args
*/
public static void main(String[] args) {
/**
* 1. create LRUCache of initial capacity 10
* 2. insert 10 objects to cache
* 3. print the cache objects* 4. access the first object and print the cache
* 5. insert new objects to cache
* 6. print the cache and observe that the least recently used
* objects are evicted
*/
// 1. initiate the cache with capacity 10
LRUCache<String, String> cache = new LRUCache<String, String>(10);
// 2. insert 10 objects to cache
for(int i=1; i<=10; i++) {
cache.put(String.format("key-%d", i), String.format("value-%d", i));
}
// 3. print the cache objects
System.out.println("printing cache:");cache.printCache();
// 4. access the first object and print the cache
cache.get("key-1");
System.out.println("printing cache after accessing key-1:");
cache.printCache();
// 5. insert new objects to cache
for(int i=11; i<=15; i++) {
cache.put(String.format("key-%d", i), String.format("value-%d", i));
}
// 6. print the cache and observe that the least recently used objects are evicted
System.out.println("printing cache after adding new objects:");
cache.printCache();
}}


往期推荐:

链接:https://dzone.com/articles/java-based-simple-cache-lru-eviction


升职加薪 就来咕泡学院
长按二维码关注


点“在看”涨薪50%哟