`
gogole_09
  • 浏览: 201363 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

缓存策略之LRU实现(基于双链表实现)

阅读更多
 

    缓存在应用中的作用,相信不用多说,对性能是具有质的提升的,而目前的缓存策略常用的FIFO,LRU等等。

   今天来探讨一下 LRU这种缓存策略的底层原理与实现。

 

  首先,来看看LRU的定义: Least recently used. 可以理解为, 最少使用的被淘汰。

  

  今天主要来讨论基于双链表的LRU算法的实现, 在讨论之前,我们需要了解一下,传统LRU算法的实现,与其的弊端。

 

   传统意义的LRU算法是为每一个Cache对象设置一个计数器,每次Cache命中则给计数器+1,而Cache用完,需要淘汰旧内容,放置新内容时,就查看所有的计数器,并将最少使用的内容替换掉。

    它的弊端很明显,如果Cache的数量少,问题不会很大, 但是如果Cache的空间过大,达到10W或者100W以上,一旦需要淘汰,则需要遍历所有计算器,其性能与资源消耗是巨大的。效率也就非常的慢了。

 

    基于这样的情况,所有就有新的LRU算法的实现----基于双链表 的LRU实现。

    它的原理: 将Cache的所有位置都用双连表连接起来,当一个位置被命中之后,就将通过调整链表的指向,将该位置调整到链表头的位置,新加入的Cache直接加到链表头中。

     这样,在多次进行Cache操作后,最近被命中的,就会被向链表头方向移动,而没有命中的,而想链表后面移动,链表尾则表示最近最少使用的Cache。

     当需要替换内容时候,链表的最后位置就是最少被命中的位置,我们只需要淘汰链表最后的部分即可。

 

  上面说了这么多的理论, 下面用代码来实现一个LRU策略的缓存。

    我们用一个对象来表示Cache,并实现双链表,

     

public class LRUCache {
	/**
	 * 链表节点
	 * @author Administrator
	 *
	 */
	class CacheNode {
		……
	}

	private int cacheSize;//缓存大小
	private Hashtable nodes;//缓存容器
	private int currentSize;//当前缓存对象数量
	private CacheNode first;//(实现双链表)链表头
	private CacheNode last;//(实现双链表)链表尾
}

 

 下面给出完整的实现,这个类也被Tomcat所使用( org.apache.tomcat.util.collections.LRUCache),但是在tomcat6.x版本中,已经被弃用,使用另外其他的缓存类来替代它。

 

public class LRUCache {
	/**
	 * 链表节点
	 * @author Administrator
	 *
	 */
	class CacheNode {
		CacheNode prev;//前一节点
		CacheNode next;//后一节点
		Object value;//值
		Object key;//键
		CacheNode() {
		}
	}

	public LRUCache(int i) {
		currentSize = 0;
		cacheSize = i;
		nodes = new Hashtable(i);//缓存容器
	}
	
	/**
	 * 获取缓存中对象
	 * @param key
	 * @return
	 */
	public Object get(Object key) {
		CacheNode node = (CacheNode) nodes.get(key);
		if (node != null) {
			moveToHead(node);
			return node.value;
		} else {
			return null;
		}
	}
	
	/**
	 * 添加缓存
	 * @param key
	 * @param value
	 */
	public void put(Object key, Object value) {
		CacheNode node = (CacheNode) nodes.get(key);
		
		if (node == null) {
			//缓存容器是否已经超过大小.
			if (currentSize >= cacheSize) {
				if (last != null)//将最少使用的删除
					nodes.remove(last.key);
				removeLast();
			} else {
				currentSize++;
			}
			
			node = new CacheNode();
		}
		node.value = value;
		node.key = key;
		//将最新使用的节点放到链表头,表示最新使用的.
		moveToHead(node);
		nodes.put(key, node);
	}

	/**
	 * 将缓存删除
	 * @param key
	 * @return
	 */
	public Object remove(Object key) {
		CacheNode node = (CacheNode) nodes.get(key);
		if (node != null) {
			if (node.prev != null) {
				node.prev.next = node.next;
			}
			if (node.next != null) {
				node.next.prev = node.prev;
			}
			if (last == node)
				last = node.prev;
			if (first == node)
				first = node.next;
		}
		return node;
	}

	public void clear() {
		first = null;
		last = null;
	}

	/**
	 * 删除链表尾部节点
	 *  表示 删除最少使用的缓存对象
	 */
	private void removeLast() {
		//链表尾不为空,则将链表尾指向null. 删除连表尾(删除最少使用的缓存对象)
		if (last != null) {
			if (last.prev != null)
				last.prev.next = null;
			else
				first = null;
			last = last.prev;
		}
	}
	
	/**
	 * 移动到链表头,表示这个节点是最新使用过的
	 * @param node
	 */
	private void moveToHead(CacheNode node) {
		if (node == first)
			return;
		if (node.prev != null)
			node.prev.next = node.next;
		if (node.next != null)
			node.next.prev = node.prev;
		if (last == node)
			last = node.prev;
		if (first != null) {
			node.next = first;
			first.prev = node;
		}
		first = node;
		node.prev = null;
		if (last == null)
			last = first;
	}
	private int cacheSize;
	private Hashtable nodes;//缓存容器
	private int currentSize;
	private CacheNode first;//链表头
	private CacheNode last;//链表尾
}

 

 PS:

   首先感谢各位给帖子投票的朋友, 不管你是投的新手贴,还是精华帖,都是对我的鼓励,

    对于帖子中大量谈到的并发问题,这个实现在写之前确实是没有考虑的。 

   帖子的本意只是向不清楚LRU实现的朋友,展示这种算法的实现而已,并非是专门讲并发性问题的。

   对于帖子中谈到的并发问题, 稍后有时间,我会写一个稍微完善一点的实现,贴出来, 到时候,再跟大家探讨探讨。

分享到:
评论
32 楼 harim 2015-04-13  
思路十分不错,最近两家公司面试都问到了这个问题,我没有答出来,遗憾啊!
31 楼 hardPass 2010-06-21  
linliangyi2007 写道
hardPass 写道


linliangyi2007 写道
[
这是算法好贴,那个家伙投的“新手”,替楼主鄙视之~~

看了楼主的源码,似乎有并发问题,不是hashtable的并发问题,而是在对cache指针做移动时,存在并发问题,不知道是不是我没考虑周全。

不过原理是讲透了,给个精华!


linliangyi2007 写道
hardPass 写道
只能投新手帖,没有深度,双重链表,大学数据结构书里就有

HashTable本身是没有并发问题,但楼主的程序有明显的并发问题。
用HashTable就没有并发问题了吗?
讲cache,不提并发,等于没讲。

在并发的时候,moveToHead有明显的并发问题
有可能出现最近使用,却被首先删除的情况。和LRU背道而驰。


javaeye就是多这样的看客,有本事自己写几个深刻的啊!不要人家写了还这么不怀好意。

就算不深刻,也不用投个新手吧(要扣分的),你牛你飘过就好!

支持楼主的精神先



不错,对楼主的乐于分享的精神,还是值得肯定的。
说句实话,我之前从来没给谁投过新手帖,一般看到讨论,围观围观就算了。

虽然楼主的代码有问题,我可以不管,但是却被你这位仁兄说成是精华,那就是另外一回事情了。
如果我们默许、纵容你等乱投精华,岂不是误导新人?那才是真正的悲哀!
你可知道,楼主被扣分,也是因为你!

je这个地方,注重的是实事求是的讨论,吹嘘拍马的就不要了吧




楼上的搞清楚孰先孰后,是帖子先被投了新手,我才投个精华来做冲抵的,不要冤枉好人哈



反正我是先看到你说要投精......
30 楼 prowl 2010-06-18  
linliangyi2007 写道
prowl 写道
并发,建议使用ConcurrentHashMap吧。效率比HashTable高很多



美眉是吧,看清楚人家在讨论啥了木有啊,人家重点在LRU算法啊,不是在讲HashTable哦。


算法不包括效率和并发性吗?
29 楼 linliangyi2007 2010-06-18  
hardPass 写道


linliangyi2007 写道
[
这是算法好贴,那个家伙投的“新手”,替楼主鄙视之~~

看了楼主的源码,似乎有并发问题,不是hashtable的并发问题,而是在对cache指针做移动时,存在并发问题,不知道是不是我没考虑周全。

不过原理是讲透了,给个精华!


linliangyi2007 写道
hardPass 写道
只能投新手帖,没有深度,双重链表,大学数据结构书里就有

HashTable本身是没有并发问题,但楼主的程序有明显的并发问题。
用HashTable就没有并发问题了吗?
讲cache,不提并发,等于没讲。

在并发的时候,moveToHead有明显的并发问题
有可能出现最近使用,却被首先删除的情况。和LRU背道而驰。


javaeye就是多这样的看客,有本事自己写几个深刻的啊!不要人家写了还这么不怀好意。

就算不深刻,也不用投个新手吧(要扣分的),你牛你飘过就好!

支持楼主的精神先



不错,对楼主的乐于分享的精神,还是值得肯定的。
说句实话,我之前从来没给谁投过新手帖,一般看到讨论,围观围观就算了。

虽然楼主的代码有问题,我可以不管,但是却被你这位仁兄说成是精华,那就是另外一回事情了。
如果我们默许、纵容你等乱投精华,岂不是误导新人?那才是真正的悲哀!
你可知道,楼主被扣分,也是因为你!

je这个地方,注重的是实事求是的讨论,吹嘘拍马的就不要了吧




楼上的搞清楚孰先孰后,是帖子先被投了新手,我才投个精华来做冲抵的,不要冤枉好人哈
28 楼 hardPass 2010-06-18  


linliangyi2007 写道
[
这是算法好贴,那个家伙投的“新手”,替楼主鄙视之~~

看了楼主的源码,似乎有并发问题,不是hashtable的并发问题,而是在对cache指针做移动时,存在并发问题,不知道是不是我没考虑周全。

不过原理是讲透了,给个精华!


linliangyi2007 写道
hardPass 写道
只能投新手帖,没有深度,双重链表,大学数据结构书里就有

HashTable本身是没有并发问题,但楼主的程序有明显的并发问题。
用HashTable就没有并发问题了吗?
讲cache,不提并发,等于没讲。

在并发的时候,moveToHead有明显的并发问题
有可能出现最近使用,却被首先删除的情况。和LRU背道而驰。


javaeye就是多这样的看客,有本事自己写几个深刻的啊!不要人家写了还这么不怀好意。

就算不深刻,也不用投个新手吧(要扣分的),你牛你飘过就好!

支持楼主的精神先



不错,对楼主的乐于分享的精神,还是值得肯定的。
说句实话,我之前从来没给谁投过新手帖,一般看到讨论,围观围观就算了。

虽然楼主的代码有问题,我可以不管,但是却被你这位仁兄说成是精华,那就是另外一回事情了。
如果我们默许、纵容你等乱投精华,岂不是误导新人?那才是真正的悲哀!
你可知道,楼主被扣分,也是因为你!

je这个地方,注重的是实事求是的讨论,吹嘘拍马的就不要了吧




27 楼 lw1130 2010-06-18  
lru 这个是oracle在 库高速缓存中就是使用的这个策略,最近最少使用 算法 呵呵
26 楼 linliangyi2007 2010-06-17  
prowl 写道
并发,建议使用ConcurrentHashMap吧。效率比HashTable高很多



美眉是吧,看清楚人家在讨论啥了木有啊,人家重点在LRU算法啊,不是在讲HashTable哦。
25 楼 linliangyi2007 2010-06-17  
hardPass 写道
只能投新手帖,没有深度,双重链表,大学数据结构书里就有

HashTable本身是没有并发问题,但楼主的程序有明显的并发问题。
用HashTable就没有并发问题了吗?
讲cache,不提并发,等于没讲。

在并发的时候,moveToHead有明显的并发问题
有可能出现最近使用,却被首先删除的情况。和LRU背道而驰。


javaeye就是多这样的看客,有本事自己写几个深刻的啊!不要人家写了还这么不怀好意。

就算不深刻,也不用投个新手吧(要扣分的),你牛你飘过就好!

支持楼主的精神先
24 楼 prowl 2010-06-17  
并发,建议使用ConcurrentHashMap吧。效率比HashTable高很多
23 楼 kerrysk 2010-06-17  
可以看下OSCache的实现,JDK有相应数据结构的实现LinkedHashSet。
22 楼 jef 2010-06-17  
hardPass 写道
有可能出现最近使用,却被首先删除的情况。和LRU背道而驰。


插句嘴,我一直认为真正的LRU实现——兼顾性能和统计的准确性,是需要内部的独立线程完成命中统计及删除操作的,命中数不重要,重要的是单位时间内的命中率。不知道我的想法对不对。


21 楼 elam 2010-06-17  
hardPass 写道
只能投新手帖,没有深度,双重链表,大学数据结构书里就有

HashTable本身是没有并发问题,但楼主的程序有明显的并发问题。
用HashTable就没有并发问题了吗?
讲cache,不提并发,等于没讲。

在并发的时候,moveToHead有明显的并发问题
有可能出现最近使用,却被首先删除的情况。和LRU背道而驰。

我也同意这个链表会出现最近使用却被最先删除的情况,觉得这个实现很不完善。
20 楼 hardPass 2010-06-17  
只能投新手帖,没有深度,双重链表,大学数据结构书里就有

HashTable本身是没有并发问题,但楼主的程序有明显的并发问题。
用HashTable就没有并发问题了吗?
讲cache,不提并发,等于没讲。

在并发的时候,moveToHead有明显的并发问题
有可能出现最近使用,却被首先删除的情况。和LRU背道而驰。
19 楼 linliangyi2007 2010-06-17  
这是算法好贴,那个家伙投的“新手”,替楼主鄙视之~~

看了楼主的源码,似乎有并发问题,不是hashtable的并发问题,而是在对cache指针做移动时,存在并发问题,不知道是不是我没考虑周全。

不过原理是讲透了,给个精华!
18 楼 giginet 2010-06-17  
确实,这个是一个原理而已。真正实现的话,还需要考虑如下问题:
1.缓存大小设定
2.硬盘存储
3.快速查找
4.并发
5.hit
17 楼 gogole_09 2010-06-16  
kimmking 写道
yimlin 写道
不晓得并发的效率如何,感觉这样并发效率会相对慢些。
因为执行操作时需要加锁,而且锁的时间较长。

所以,必须要快速的命中,
否则,无法实用的。

如果是db来的数据缓存,一般很高的命中率(比如90%),才有使用价值。

 
上述提到的问题,确实是此实现需要解决的,也是我没有考虑的, 事实上我的初衷是将LRU缓存的原理解释一下, 对于并发性,命中率等还没有进行深入考虑,可以说只是将基本原理说明一下, 当然离一个真正投入使用的LRU缓存实现还差很多, 在此多谢此帖中各位朋友提出的相关问题与提供的解决方案的参考。
16 楼 gogole_09 2010-06-16  
<div class="quote_title">zhangshixi 写道</div>
<div class="quote_div">
<p>LinkedHashMap的LRU策略:<a href="http://zhangshixi.iteye.com/blog/673789" target="_self">http://zhangshixi.iteye.com/blog/673789</a></p>
</div>
<p><br>     此文分析很透彻, 受教.</p>
15 楼 gogole_09 2010-06-16  
icanfly 写道
我看这缓存实现没有带锁?

  实现利用Hashtable充当容器, Hashtable为线程安全,所以不需加锁.
14 楼 kimmking 2010-06-16  
yimlin 写道
不晓得并发的效率如何,感觉这样并发效率会相对慢些。
因为执行操作时需要加锁,而且锁的时间较长。

所以,必须要快速的命中,
否则,无法实用的。

如果是db来的数据缓存,一般很高的命中率(比如90%),才有使用价值。
13 楼 yimlin 2010-06-16  
不晓得并发的效率如何,感觉这样并发效率会相对慢些。
因为执行操作时需要加锁,而且锁的时间较长。

相关推荐

Global site tag (gtag.js) - Google Analytics