您的位置:首页 > 其它

浅析LRU(K-V)缓存

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

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

wikipedia对LRU的描述:

Incomputing,cachealgorithms(alsofrequentlycalledcachereplacementalgorithmsorcachereplacementpolicies)areoptimizinginstructions—​oralgorithms—​thatacomputerprogramorahardware-maintainedstructurecanfollowinordertomanageacacheofinformationstoredonthecomputer.Whenthecacheisfull,thealgorithmmustchoosewhichitemstodiscardtomakeroomforthenewones.

LeastRecentlyUsed(LRU)

Discardstheleastrecentlyuseditemsfirst.Thisalgorithmrequireskeepingtrackofwhatwasusedwhen,whichisexpensiveifonewantstomakesurethealgorithmalwaysdiscardstheleastrecentlyuseditem.Generalimplementationsofthistechniquerequirekeeping"agebits"forcache-linesandtrackthe"LeastRecentlyUsed"cache-linebasedonage-bits.Insuchanimplementation,everytimeacache-lineisused,theageofallothercache-lineschanges.LRUisactuallyafamilyofcachingalgorithmswithmembersincluding2QbyTheodoreJohnsonandDennisShasha,[3]andLRU/KbyPatO'Neil,BettyO'NeilandGerhardWeikum.[4]

LRUCache的分析实现

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

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

privateintcapacity;

privatejava.util.LinkedHashMap<Integer,Integer>cache=newjava.util.LinkedHashMap<Integer,Integer>(){
  @Override
protectedbooleanremoveEldestEntry(Map.Entry<Integer,Integer>eldest){
returnsize()>capacity;
}
};


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

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

继承:

packagelrucache.one;

importjava.util.LinkedHashMap;
importjava.util.Map;

/**
*LRUCache的LinkedHashMap实现,继承。
*@authorwxisme
*@time2015-10-18上午10:27:37
*/
publicclassLRUCacheextendsLinkedHashMap<Integer,Integer>{

privateintinitialCapacity;

publicLRUCache(intinitialCapacity){
super(initialCapacity,0.75f,true);
this.initialCapacity=initialCapacity;
}

@Override
protectedbooleanremoveEldestEntry(
Map.Entry<Integer,Integer>eldest){
returnsize()>initialCapacity;
}

@Override
publicStringtoString(){

StringBuildercacheStr=newStringBuilder();
cacheStr.append("{");

for(Map.Entry<Integer,Integer>entry:this.entrySet()){
cacheStr.append("["+entry.getKey()+","+entry.getValue()+"]");
}
cacheStr.append("}");
returncacheStr.toString();
}

}


组合:

packagelrucache.three;

importjava.util.LinkedHashMap;
importjava.util.Map;

/**
*LRUCache的LinkedHashMap实现,组合
*@authorwxisme
*@time2015-10-18上午11:07:01
*/
publicclassLRUCache{

privatefinalintinitialCapacity;

privateMap<Integer,Integer>cache;

publicLRUCache(finalintinitialCapacity){
this.initialCapacity=initialCapacity;
cache=newLinkedHashMap<Integer,Integer>(initialCapacity,0.75f,true){
@Override
protectedbooleanremoveEldestEntry(
Map.Entry<Integer,Integer>eldest){
returnsize()>initialCapacity;
}
};
}

publicvoidput(intkey,intvalue){
cache.put(key,value);
}

publicintget(intkey){
returncache.get(key);
}

publicvoidremove(intkey){
cache.remove(key);
}

@Override
publicStringtoString(){

StringBuildercacheStr=newStringBuilder();
cacheStr.append("{");

for(Map.Entry<Integer,Integer>entry:cache.entrySet()){
cacheStr.append("["+entry.getKey()+","+entry.getValue()+"]");
}
cacheStr.append("}");
returncacheStr.toString();
}
}


测试代码:

publicstaticvoidmain(String[]args){

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

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

packagelrucache.tow;

importjava.util.HashMap;
importjava.util.Map;

/**
*LRUCache链表+HashMap实现
*@authorwxisme
*@time2015-10-18下午12:34:36
*/
publicclassLRUCache<K,V>{

privatefinalintinitialCapacity;//容量

privateNodehead;//头结点
privateNodetail;//尾结点

privateMap<K,Node<K,V>>map;

publicLRUCache(intinitialCapacity){
this.initialCapacity=initialCapacity;
map=newHashMap<K,Node<K,V>>();
}

/**
*双向链表的节点
*@authorwxisme
*
*@param<K>
*@param<V>
*/
privateclassNode<K,V>{
publicNodepre;
publicNodenext;
publicKkey;
publicVvalue;

publicNode(){}

publicNode(Kkey,Vvalue){
this.key=key;
this.value=value;
}

}

/**
*向缓存中添加一个K,V
*@paramkey
*@paramvalue
*/
publicvoidput(Kkey,Vvalue){
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=newNode();
node.key=key;
}
node.value=value;
moveToHead(node);
map.put(key,node);
}

/**
*从缓存中获取一个K,V
*@paramkey
*@returnv
*/
publicVget(Kkey){
Node<K,V>node=map.get(key);
if(node==null){
returnnull;
}
//最近访问,移动到头部。
moveToHead(node);
returnnode.value;
}

/**
*从缓存中删除K,V
*@paramkey
*/
publicvoidremove(Kkey){
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移动到链表头部
*@paramnode
*/
privatevoidmoveToHead(Nodenode){

//切断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;

}

/**
*删除链表的尾结点
*/
privatevoidremoveTailNode(){
if(tail!=null){
tail=tail.pre;
tail.next=null;
}
}

@Override
publicStringtoString(){

StringBuildercacheStr=newStringBuilder();
cacheStr.append("{");
//因为元素的访问顺序是在链表里维护的,这里要遍历链表
Node<K,V>node=head;
while(node!=null){
cacheStr.append("["+node.key+","+node.value+"]");
node=node.next;
}

cacheStr.append("}");

returncacheStr.toString();
}

}


测试数据:

publicstaticvoidmain(String[]args){

LRUCache<Integer,Integer>cache=newLRUCache<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却还是一种插入顺序?

深入源码分析一下:

privatestaticfinallongserialVersionUID=3801124242820219131L;

/**
*Theheadofthedoublylinkedlist.
*/
privatetransientEntry<K,V>header;

/**
*Theiterationorderingmethodforthislinkedhashmap:<tt>true</tt>
*foraccess-order,<tt>false</tt>forinsertion-order.
*
*@serial
*/
privatefinalbooleanaccessOrder;


/**
*LinkedHashMapentry.
*/
privatestaticclassEntry<K,V>extendsHashMap.Entry<K,V>{
//Thesefieldscomprisethedoublylinkedlistusedforiteration.
Entry<K,V>before,after;

Entry(inthash,Kkey,Vvalue,HashMap.Entry<K,V>next){
super(hash,key,value,next);
}


privatetransientEntry<K,V>header;

privatestaticclassEntry<K,V>extendsHashMap.Entry<K,V>{
 Entry<K,V>before,after;
……
}


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

/**
*Constructsanempty<tt>LinkedHashMap</tt>instancewiththe
*specifiedinitialcapacity,loadfactorandorderingmode.
*
*@paraminitialCapacitytheinitialcapacity
*@paramloadFactortheloadfactor
*@paramaccessOrdertheorderingmode-<tt>true</tt>for
*access-order,<tt>false</tt>forinsertion-order
*@throwsIllegalArgumentExceptioniftheinitialcapacityisnegative
*ortheloadfactorisnonpositive
*/
publicLinkedHashMap(intinitialCapacity,
floatloadFactor,
booleanaccessOrder){
super(initialCapacity,loadFactor);
this.accessOrder=accessOrder;
}


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

publicVget(Objectkey){
Entry<K,V>e=(Entry<K,V>)getEntry(key);
if(e==null)
returnnull;
e.recordAccess(this);
returne.value;
}


voidrecordAccess(HashMap<K,V>m){
LinkedHashMap<K,V>lm=(LinkedHashMap<K,V>)m;
if(lm.accessOrder){
lm.modCount++;
remove();
addBefore(lm.header);
}
}

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



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

/**
*Insertsthisentrybeforethespecifiedexistingentryinthelist.
*/
privatevoidaddBefore(Entry<K,V>existingEntry){
after=existingEntry;
before=existingEntry.before;
before.after=this;
after.before=this;
}


voidaddEntry(inthash,Kkey,Vvalue,intbucketIndex){
createEntry(hash,key,value,bucketIndex);

//Removeeldestentryifinstructed,elsegrowcapacityifappropriate
Entry<K,V>eldest=header.after;
if(removeEldestEntry(eldest)){
removeEntryForKey(eldest.key);
}else{
if(size>=threshold)
resize(2*table.length);
}
}

/**
*ThisoverridediffersfromaddEntryinthatitdoesn'tresizethe
*tableorremovetheeldestentry.
*/
voidcreateEntry(inthash,Kkey,Vvalue,intbucketIndex){
HashMap.Entry<K,V>old=table[bucketIndex];
Entry<K,V>e=newEntry<K,V>(hash,key,value,old);
table[bucketIndex]=e;
e.addBefore(header);
size++;
}


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

拓展一下:

publicHashMap(intinitialCapacity,floatloadFactor){
if(initialCapacity<0)
thrownewIllegalArgumentException("Illegalinitialcapacity:"+
initialCapacity);
if(initialCapacity>MAXIMUM_CAPACITY)
initialCapacity=MAXIMUM_CAPACITY;
if(loadFactor<=0||Float.isNaN(loadFactor))
thrownewIllegalArgumentException("Illegalloadfactor:"+
loadFactor);

//Findapowerof2>=initialCapacity
intcapacity=1;
while(capacity<initialCapacity)
capacity<<=1;

this.loadFactor=loadFactor;
threshold=(int)(capacity*loadFactor);
table=newEntry[capacity];
init();
}


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

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

JDK中的LinkedHashMap实现效率还是很高的。可以看一个LeetCode的中的应用:/article/5225763.html

参考资料:
http://www.cnblogs.com/lzrabbit/p/3734850.html#f1https://en.wikipedia.org/wiki/Cache_algorithms#LRUhttp://zhangshixi.iteye.com/blog/673789
如有错误,敬请指正。

转载请指明来源:/article/5225764.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: