
什么是LRU
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
使用LinkedHashMap实现

思路:
1、新数据插入到链表头部;
2、每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3、当链表满的时候,将链表尾部的数据丢弃。
代码实现
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//类说明:利用LinkedHashMap实现简单的缓存, 必须实现removeEldestEntry方法,具体参见JDK文档
public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private final int maxCapacity;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
private final Lock lock = new ReentrantLock();
public LRULinkedHashMap(int maxCapacity) {
super(maxCapacity, DEFAULT_LOAD_FACTOR, true);
this.maxCapacity = maxCapacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return super.removeEldestEntry(eldest);
}
@Override
public boolean containsKey(Object key) {
try {
lock.lock();
return super.containsKey(key);
}finally {
lock.unlock();
}
}
@Override
public V get(Object key) {
try {
lock.lock();
return super.get(key);
}finally {
lock.unlock();
}
}
@Override
public V put(K key, V value) {
try {
lock.lock();
return super.put(key, value);
}finally {
lock.unlock();
}
}
@Override
public int size() {
try {
lock.lock();
return super.size();
}finally {
lock.unlock();
}
}
@Override
public void clear() {
try {
lock.lock();
super.clear();
}finally {
lock.unlock();
}
}
public Collection<Map.Entry<K, V>> getAll() {
try {
lock.lock();
return new ArrayList<Map.Entry<K, V>>(super.entrySet());
}finally {
lock.unlock();
}
}
}
分析
【命 中率】
当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。
【复杂度】实现简单。
【代价】 命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。
HashMap 和 双向链表实现 LRU
整体的设计思路可以使用 HashMap 存储 key,这样可以做到 save 和 get key的时间都是 O(1),而 HashMap 的 Value 指向双向链表实现的 LRU 的 Node 节点,如图所示。

LRU 存储是基于双向链表实现的,下面的图演示了它的原理。 其中 head 代表双向链表的表头,tail 代表尾部。首先预先设置 LRU 的容量,如果存储满了,可以通过 O(1) 的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过 O(1)的效率把新的节点增加到对头,或者把已经存在的节点移动到队头。
下面展示了,预设大小是 3 的,LRU存储的在存储和访问过程中的变化。 为了简化图复杂度,图中没有展示 HashMap部分的变化,仅仅演示了上图 LRU 双向链表的变化。我们对这个LRU缓存的操作序列如下:
save("key1", 7)
save("key2", 0)
save("key3", 1)
save("key4", 2)
get("key2")
save("key5", 3)
get("key2")
save("key6", 4)
相应的 LRU 双向链表部分变化如下:

总结一下核心操作的步骤:
1、save(key, value) ,首先在 HashMap 找到 Key 对应的节点,如果节点存在,更新节点的值,并把这个节点移动队头。如果不存在,需要构造新的节点,并且尝试把节点塞到队头,如果LRU空间不足,则通过 tail 淘汰掉队尾的节点,同时在 HashMap 中移除 Key。
2、get(key) ,通过 HashMap 找到 LRU 链表节点,因为根据LRU 原理,这个节点是最新访问的,所以要把节点插入到队头,然后返回缓存的值。
代码实现: 非线程安全,若实现安全,则在响应的方法加锁。(如果要线程安全,可将hashmap换成线程安全的hashtable等)
import
java.
util.
HashMap;
public
class
LRUCache
<K
, V
>
{
private
int currentCacheSize
;
private
int
CacheCapcity;
private
HashMap<
K,
CacheNode
> caches
;
private
CacheNode first
;
private
CacheNode last
;
public
LRUCache(
int size
)
{
currentCacheSize
=
0;
this.
CacheCapcity
= size
;
caches
=
new
HashMap<
K,
CacheNode>(
size);
}
public
void put
(K k
,V v
){
CacheNode node
= caches
.get
(k
);
if(
node ==
null
){
if(
caches.
size()
>=
CacheCapcity
){
caches
.remove
(last
.key
);
removeLast
();
}
node
=
new
CacheNode();
node
.key
= k
;
}
node
.value
= v
;
moveToFirst
(node
);
caches
.put
(k
, node
);
}
public
Object get
(K k
){
CacheNode node
= caches
.get
(k
);
if(
node ==
null
){
return
null;
}
moveToFirst
(node
);
return node
.value
;
}
public
Object remove
(K k
){
CacheNode node
= caches
.get
(k
);
if(
node !=
null
){
if(
node.
pre !=
null
){
node
.pre
.next
=node
.next
;
}
if(
node.
next !=
null
){
node
.next
.pre
=node
.pre
;
}
if(
node ==
first){
first
= node
.next
;
}
if(
node ==
last){
last
= node
.pre
;
}
}
return caches
.remove
(k
);
}
public
void clear
(){
first
=
null;
last
=
null;
caches
.clear
();
}
private
void moveToFirst
(CacheNode
node){
if(
first ==
node){
return;
}
if(
node.
next !=
null
){
node
.next
.pre
= node
.pre
;
}
if(
node.
pre !=
null
){
node
.pre
.next
= node
.next
;
}
if(
node ==
last){
last
= last
.pre
;
}
if(
first ==
null
||
last ==
null
){
first
= last
= node
;
return;
}
node
.next
=first
;
first
.pre
= node
;
first
= node
;
first
.pre
=null
;
}
private
void removeLast
(){
if(
last !=
null
){
last
= last
.pre
;
if(
last ==
null
){
first
=
null;
}else
{
last
.next
=
null;
}
}
}
@Override
public
String toString
(){
StringBuilder sb
=
new
StringBuilder();
CacheNode node
= first
;
while(
node !=
null
){
sb
.append
(String
.format
("%s:%s "
, node
.key
,node
.value
));
node
= node
.next
;
}
return sb
.toString
();
}
class
CacheNode{
CacheNode pre
;
CacheNode next
;
Object key
;
Object value
;
public
CacheNode(){
}
}
public
static
void main
(String
[] args
)
{
LRUCache<
Integer,
String>
lru =
new
LRUCache
<Integer
,String
>(3
);
lru
.put
(1
,
"a");
// 1:a
System.
out.
println(
lru.
toString());
lru
.put
(2
,
"b");
// 2:b 1:a
System.
out.
println(
lru.
toString());
lru
.put
(3
,
"c");
// 3:c 2:b 1:a
System.
out.
println(
lru.
toString());
lru
.put
(4
,
"d");
// 4:d 3:c 2:b
System.
out.
println(
lru.
toString());
lru
.put
(1
,
"aa");
// 1:aa 4:d 3:c
System.
out.
println(
lru.
toString());
lru
.put
(2
,
"bb");
// 2:bb 1:aa 4:d
System.
out.
println(
lru.
toString());
lru
.put
(5
,
"e");
// 5:e 2:bb 1:aa
System.
out.
println(
lru.
toString());
lru
.get
(1
);
// 1:aa 5:e 2:bb
System.
out.
println(
lru.
toString());
lru
.remove
(11
);
// 1:aa 5:e 2:bb
System.
out.
println(
lru.
toString());
lru
.remove
(1
);
//5:e 2:bb
System.
out.
println(
lru.
toString());
lru
.put
(1
,
"aaa");
//1:aaa 5:e 2:bb
System.
out.
println(
lru.
toString());
}
}
参考 文档:
https://zhuanlan.zhihu.com/p/34133067
https://www.jianshu.com/p/62e829c37adf
推荐阅读
