LRU(Least Recently Used)算法是缓存技术中的一种常见思想,顾名思义,最近最少使用,也就是说有两个维度来衡量,一个是时间(最近),一个频率(最少)。如果需要按优先级来对缓存中的K-V实体进行排序的话,需要考虑这两个维度,在LRU中,最近使用频率最高的排在前面,也可以简单的说最近访问的排在前面。这就是LRU的大体思想。

在操作系统中,LRU是用来进行内存管理的页面置换算法,对于在内存中但又不用的数据块(内存块)叫做LRU,操作系统会根据哪些数据属于LRU而将其移出内存而腾出空间来加载另外的数据。

wikipedia对LRU的描述:

In computingcache algorithms (also frequently called cache replacement algorithms or cache replacement policies) are optimizinginstructions—​or algorithms—​that a computer program or a hardware-maintained structure can follow in order to manage a cache of information stored on the computer. When the cache is full, the algorithm must choose which items to discard to make room for the new ones.

Least Recently Used (LRU)

Discards the least recently used items first. This algorithm requires keeping track of what was used when, which is expensive if one wants to make sure the algorithm always discards the least recently used item. General implementations of this technique require keeping "age bits" for cache-lines and track the "Least Recently Used" cache-line based on age-bits. In such an implementation, every time a cache-line is used, the age of all other cache-lines changes. LRU is actually a family of caching algorithms with members including 2Q by Theodore Johnson and Dennis Shasha,[3] and LRU/K by Pat O'Neil, Betty O'Neil and Gerhard Weikum.[4]

LRUCache的分析实现

1.首先可以先实现一个FIFO的版本,但是这样只是以插入顺序来确定优先级的,没有考虑访问顺序,并没有完全实现LRUCache。

用Java中的LinkedHashMap实现非常简单。

private int capacity;

private java.util.LinkedHashMap<Integer, Integer> cache = new java.util.LinkedHashMap<Integer, Integer>() {
  @Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
};

程序中重写了removeEldestEntry()方法,如果大小超过了设置的容量就删除优先级最低的元素,在 FIFO版本中优先级最低的为最先插入的元素。

2.如果足够了解LinkedHashMap,实现LRUCache也是非常简单的。在LinkedHashMap中提供了可以设置容量、装载因子和顺序的构造方法。如果要实现LRUCache就可以把顺序的参数设置成true,代表访问顺序,而不是默认的FIFO的插入顺序。这里把装载因子设置为默认的0.75。并且还要重写removeEldestEntry()方法来维持当前的容量。这样一来可以有两种方法来实现LinkedHashMap版本的LRUCache。一种是继承一种是组合。

继承:

package lrucache.one;

import java.util.LinkedHashMap;
import java.util.Map; /**
*LRU Cache的LinkedHashMap实现,继承。
*@author wxisme
*@time 2015-10-18 上午10:27:37
*/
public class LRUCache extends LinkedHashMap<Integer, Integer>{ private int initialCapacity; public LRUCache(int initialCapacity) {
super(initialCapacity,0.75f,true);
this.initialCapacity = initialCapacity;
} @Override
protected boolean removeEldestEntry(
Map.Entry<Integer, Integer> eldest) {
return size() > initialCapacity;
} @Override
public String toString() { StringBuilder cacheStr = new StringBuilder();
cacheStr.append("{"); for (Map.Entry<Integer, Integer> entry : this.entrySet()) {
cacheStr.append("[" + entry.getKey() + "," + entry.getValue() + "]");
}
cacheStr.append("}");
return cacheStr.toString();
} }

组合:

package lrucache.three;

import java.util.LinkedHashMap;
import java.util.Map; /**
*LRU Cache 的LinkedHashMap实现,组合
*@author wxisme
*@time 2015-10-18 上午11:07:01
*/
public class LRUCache { private final int initialCapacity; private Map<Integer, Integer> cache; public LRUCache(final int initialCapacity) {
this.initialCapacity = initialCapacity;
cache = new LinkedHashMap<Integer, Integer>(initialCapacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<Integer, Integer> eldest) {
return size() > initialCapacity;
}
};
} public void put(int key, int value) {
cache.put(key, value);
} public int get(int key) {
return cache.get(key);
} public void remove(int key) {
cache.remove(key);
} @Override
public String toString() { StringBuilder cacheStr = new StringBuilder();
cacheStr.append("{"); for (Map.Entry<Integer, Integer> entry : cache.entrySet()) {
cacheStr.append("[" + entry.getKey() + "," + entry.getValue() + "]");
}
cacheStr.append("}");
return cacheStr.toString();
}
}

测试代码:

public static void main(String[] args) {

        LRUCache cache = new LRUCache(5);

        cache.put(5, 5);
cache.put(4, 4);
cache.put(3, 3);
cache.put(2, 2);
cache.put(1, 1); System.out.println(cache.toString()); cache.put(0, 0); System.out.println(cache.toString()); }

运行结果:

{[5,5][4,4][3,3][2,2][1,1]}
{[4,4][3,3][2,2][1,1][0,0]}

 可见已经实现了LRUCache的基本功能。

3.如果不用Java API提供的LinkedHashMap该如何实现LRU算法呢?首先我们要确定操作,LRU算法中的操作无非是插入、删除、查找并且要维护一定的顺序,这样我们有很多种选择,可以用数组,链表,栈,队列,Map中的一种或几种。先看栈和队列,虽然可以明确顺序实现FIFO或者FILO,但是LRU中是需要对两端操作的,既需要删除tail元素又需要移动head元素,可以想象效率是不理想的。我们要明确一个事实,数组和Map的只读操作复杂度为O(1),非只读操作的复杂度为O(n)。链式结构则相反。这么一来我们如果只使用其中的一种必定在只读或非只读操作上耗时过多。那我们大可以选择链表+Map组合结构。如果选择单向链表在对链表两端操作的时候还是要耗时O(n)。综上考虑,双向链表+Map结构应该是最好的。

在这种实现方式中,用双向链表来维护优先级顺序,也就是访问顺序。实现非只读操作。用Map存储K-V值,实现只读操作。访问顺序:最近访问(插入也是一种访问)的移动到链表头部,如果达到上限则删除链表尾部的元素。

 package lrucache.tow;

 import java.util.HashMap;
import java.util.Map; /**
*LRUCache链表+HashMap实现
*@author wxisme
*@time 2015-10-18 下午12:34:36
*/
public class LRUCache<K, V> { private final int initialCapacity; //容量 private Node head; //头结点
private Node tail; //尾结点 private Map<K, Node<K, V>> map; public LRUCache(int initialCapacity) {
this.initialCapacity = initialCapacity;
map = new HashMap<K, Node<K, V>>();
} /**
* 双向链表的节点
* @author wxisme
*
* @param <K>
* @param <V>
*/
private class Node<K, V> {
public Node pre;
public Node next;
public K key;
public V value; public Node(){} public Node(K key, V value) {
this.key = key;
this.value = value;
} } /**
* 向缓存中添加一个K,V
* @param key
* @param value
*/
public void put(K key, V value) {
Node<K, V> node = map.get(key); //node不在缓存中
if(node == null) {
//此时,缓存已满
if(map.size() >= this.initialCapacity) {
map.remove(tail.key); //在map中删除最久没有use的K,V
removeTailNode();
}
node = new Node();
node.key = key;
}
node.value = value;
moveToHead(node);
map.put(key, node);
} /**
* 从缓存中获取一个K,V
* @param key
* @return v
*/
public V get(K key) {
Node<K, V> node = map.get(key);
if(node == null) {
return null;
}
//最近访问,移动到头部。
moveToHead(node);
return node.value;
} /**
* 从缓存中删除K,V
* @param key
*/
public void remove(K key) {
Node<K, V> node = map.get(key); map.remove(key); //从hashmap中删除 //在双向链表中删除
if(node != null) {
if(node.pre != null) {
node.pre.next = node.next;
}
if(node.next != null) {
node.next.pre = node.pre;
}
if(node == head) {
head = head.next;
}
if(node == tail) {
tail = tail.pre;
} //除去node的引用
node.pre = null;
node.next = null;
node = null;
} } /**
* 把node移动到链表头部
* @param node
*/
private void moveToHead(Node node) { //切断node if(node == head) return ; if(node.pre !=null) {
node.pre.next = node.next;
}
if(node.next != null) {
node.next.pre = node.pre;
}
if(node == tail) {
tail = tail.pre;
} if(tail == null || head == null) {
tail = head = node;
return ;
} //把node移送到head
node.next = head;
head.pre = node;
head = node;
node.pre = null; } /**
* 删除链表的尾结点
*/
private void removeTailNode() {
if(tail != null) {
tail = tail.pre;
tail.next = null;
}
} @Override
public String toString() { StringBuilder cacheStr = new StringBuilder();
cacheStr.append("{");
//因为元素的访问顺序是在链表里维护的,这里要遍历链表
Node<K, V> node = head;
while(node != null) {
cacheStr.append("[" + node.key + "," + node.value + "]");
node = node.next;
} cacheStr.append("}"); return cacheStr.toString();
} }

测试数据:

public static void main(String[] args) {

    LRUCache<Integer, Integer> cache = new LRUCache<Integer, Integer>(5);

    cache.put(5, 5);
cache.put(4, 4);
cache.put(3, 3);
cache.put(2, 2);
cache.put(1, 1); System.out.println(cache.toString()); cache.put(0, 0); System.out.println(cache.toString()); }

运行结果:

{[1,1][2,2][3,3][4,4][5,5]}
{[0,0][1,1][2,2][3,3][4,4]}

 也实现了LRUCache的基本操作。

等等!一样的测试数据为什么结果和上面LinkedHashMap实现不一样!

细心观察可能会发现,虽然都实现了LRU,但是双向链表+HashMap确实是访问顺序,而LinkedHashMap却还是一种插入顺序?

深入源码分析一下:

private static final long serialVersionUID = 3801124242820219131L;

    /**
* The head of the doubly linked list.
*/
private transient Entry<K,V> header; /**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
private final boolean accessOrder;
 /**
* LinkedHashMap entry.
*/
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
private transient Entry<K,V> header;

private static class Entry<K,V> extends HashMap.Entry<K,V> {
&nbsp; Entry<K,V> before, after;
……
}

从上面的代码片段可以看出,LinkedHashMap也是使用了双向链表,而且使用了Map中的Hash算法。LinkedHashMap是继承了HashMap,实现了Map的。

/**
* Constructs an empty <tt>LinkedHashMap</tt> instance with the
* specified initial capacity, load factor and ordering mode.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @param accessOrder the ordering mode - <tt>true</tt> for
* access-order, <tt>false</tt> for insertion-order
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}

上面的代码是我们使用的构造方法。

public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}

void recordRemoval(HashMap<K,V> m) {
  remove();
}

 

这是实现访问顺序的关键代码。

/**
* Inserts this entry before the specified existing entry in the list.
*/
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
createEntry(hash, key, value, bucketIndex); // Remove eldest entry if instructed, else grow capacity if appropriate
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
if (size >= threshold)
resize(2 * table.length);
}
} /**
* This override differs from addEntry in that it doesn't resize the
* table or remove the eldest entry.
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}

通过这两段代码我们可以知道,出现上面问题的原因是实现访问顺序的方式不一样,链表+HashMap是访问顺序优先级从前往后,而LinkedHashMap中是相反的。

拓展一下:

public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor); // Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1; this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}

上面这段代码是HashMap的初始化代码,可以知道,初始容量是设置为1的,然后不断的加倍知道大于设置的容量为止。这是一种节省存储的做法。如果设置了装载因子,在后续的扩充操作中容量是初始设置容量和装载因子之积。

上面的所有实现都是单线程的。在并发的情况下不适用。可以使用java.util.concurrent包下的工具类和Collections工具类进行并发改造。

JDK中的LinkedHashMap实现效率还是很高的。可以看一个LeetCode的中的应用:http://www.cnblogs.com/wxisme/p/4888648.html

参考资料:

http://www.cnblogs.com/lzrabbit/p/3734850.html#f1

https://en.wikipedia.org/wiki/Cache_algorithms#LRU

http://zhangshixi.iteye.com/blog/673789

如有错误,敬请指正。

转载请指明来源:http://www.cnblogs.com/wxisme/p/4889846.html

浅析LRU(K-V)缓存的更多相关文章

  1. 简单LRU算法实现缓存

    最简单的LRU算法实现,就是利用jdk的LinkedHashMap,覆写其中的removeEldestEntry(Map.Entry)方法即可,如下所示: java 代码 import java.ut ...

  2. GuavaCache学习笔记一:自定义LRU算法的缓存实现

    前言 今天在看GuavaCache缓存相关的源码,这里想到先自己手动实现一个LRU算法.于是乎便想到LinkedHashMap和LinkedList+HashMap, 这里仅仅是作为简单的复习一下. ...

  3. 关于时间序列数据库的思考——(1)运用hash文件(例如:RRD,Whisper) (2)运用LSM树来备份(例如:LevelDB,RocksDB,Cassandra) (3)运用B-树排序和k/v存储(例如:BoltDB,LMDB)

    转自:http://0351slc.com/portal.php?mod=view&aid=12 近期网络上呈现了有关catena.benchmarking boltdb等时刻序列存储办法的介 ...

  4. cache4j轻量级java内存缓存框架,实现FIFO、LRU、TwoQueues缓存模型

    简介 cache4j是一款轻量级java内存缓存框架,实现FIFO.LRU.TwoQueues缓存模型,使用非常方便. cache4j为java开发者提供一种更加轻便的内存缓存方案,杀鸡焉用EhCac ...

  5. java:警告:[unchecked] 对作为普通类型 java.util.HashMap 的成员的put(K,V) 的调用未经检查

    java:警告:[unchecked] 对作为普通类型 java.util.HashMap 的成员的put(K,V) 的调用未经检查 一.问题:学习HashMap时候,我做了这样一个程序: impor ...

  6. Java集合源码分析(七)HashMap<K, V>

    一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap  ...

  7. JAVA泛型? T K V E等代表的意思

    ? 表示不确定的java类型. T  表示java类型. K V 分别代表java键值中的Key Value. E 代表Element. Object跟这些东西代表的java类型有啥区别呢? Obje ...

  8. Array,ArrayList、List<T>、HashSet<T>、LinkedList<T>与Dictionary<K,V>

    Array: 数组在C#中最早出现的.在内存中是连续存储的,所以它的索引速度非常快,而且赋值与修改元素也很简单. 但是数组存在一些不足的地方.在数组的两个数据间插入数据是很麻烦的,而且在声明数组的时候 ...

  9. ArrayList,Hashtable,List<T>,Dictionary<K,V>

    1.ArrayList ArrayList list = new ArrayList(); //for遍历 ; i < list.Count; i++) { SE se=(SE)list[i]; ...

随机推荐

  1. c#之第二课

    输出语句: /////////////////////////////// public class Hello1 { public static void Main() { System.Conso ...

  2. 小白学数据分析----->DNU/DAU

    行业指标观察分析-DNU/DAU 写在分析之前 一直以来,我们对于数据都是在做加法,也希望这个过程中,不断搜罗和变换出来更多的数据指标,维度等等.而在实际的分析中,我们发现,一如我们给用户提供产品一样 ...

  3. 查看Exchange邮件队列(queue)

    #加载Exchange管理模块 Add-PSSnapin Microsoft.Exchange.Management.PowerShell.snapin get-queue #查看队列 get-mes ...

  4. js获取gridview模板列中textbox行列的值

    下面一个例子:在gridview中第一列输入数值,第二列输入数值,点击第三列的时候进行计算 求和,如果不符合标记为红色字体. 如图: 代码 : <html xmlns="http:// ...

  5. 每日英语:Mistrust Between U.S., Malaysia Strains Probe

    Mistrust between U.S. and Malaysian air-accident investigators has hampered a multinational probe in ...

  6. 一篇文章,读懂 Netty 的高性能架构之道

    原文 Netty是一个高性能.异步事件驱动的NIO框架,它提供了对TCP.UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机 ...

  7. android apk--程序发布前的准备

    摘自:http://www.cnblogs.com/androidsuperman/p/4396889.html 首先,需要准备的工作: 1   用户协议(可以是本地html资源,也可以是通过webv ...

  8. 【转】JavaScript中的对象复制(Object Clone)

    JavaScript中并没有直接提供对象复制(Object Clone)的方法.因此下面的代码中改变对象b的时候,也就改变了对象a. a = {k1:1, k2:2, k3:3}; b = a; b. ...

  9. 整理PHP_YII环境安装遇到的一些问题

    安装yii遇到的一些问题 操作环境 一.Permissiondenied问题 在终端执行如下命令(注意因为是本地测试环境不需要考虑太多权限问题,如果正式环境请慎重) sudo chmod -R o+r ...

  10. 解决CSS移动端1px边框问题

    移动项目开发中,安卓或者IOS等高分辨率屏幕会把1px的border渲染成2px来显示,网上搜了一下,解决方法如下: 一.利用css中的transform的缩放属性解决,推荐这个.如下面代码. < ...