lru与lirs

(1)LRU
LRU算法淘汰最长时间没有读或者写过的数据。就以LinkedHashMap为例来说明怎样实现一个LRU算法。

首先先看一下LinkedHashMap怎么用的。

package com.demo.bean.zwfz;
 
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
 
public class LRULinkedMap<K, V> {
 
	/**
     * 最大缓存大小
     */
	private int cacheSize;
	
	private LinkedHashMap<K, V> cacheMap;
	
	public LRULinkedMap(int cacheSize){
		this.cacheSize = cacheSize;
		
		cacheMap = new LinkedHashMap(16, 0.75F, true){
 
			@Override
			protected boolean removeEldestEntry(Entry eldest) {
				if(cacheSize + 1 == cacheMap.size()){
					return true;
				}else{
					return false;
				}
			}
		};
	}
	
	public void put(K key, V value){
		cacheMap.put(key, value);
	}
	
	public V get(K key){
		return cacheMap.get(key);
	}
	
	public Collection<Map.Entry<K, V>> getAll(){
		return new ArrayList<Map.Entry<K, V>>(cacheMap.entrySet());
	}
	
	public static void main(String[] args) {
		LRULinkedMap<String, Integer> map = new LRULinkedMap<>(3);
		map.put("key1", 1);
		map.put("key2", 2);
		map.put("key3", 3);
		map.put("key8", 99);

		for (Map.Entry<String, Integer> e : map.getAll()){
			System.out.println(e.getKey()+"====>"+e.getValue());
		}
		System.out.println("\n");
		map.put("key4", 4);
		for (Map.Entry<String, Integer> e : map.getAll()){
			System.out.println(e.getKey()+"====>"+e.getValue());
		}
		
	}
	 
}

下面我们聊聊LinkedHashMap。

LinkedHashMap概述:
该集合类使用LRU算法实现的HashMap,它定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
LinkedHashMap实现:
底层使用哈希表与双向链表来保存所有元素。其基本操作与父类HashMap相似,它通过重写父类相关的方法,来实现自己的链接列表特性。
我们知道LinkedHashMap继承HashMap,所以数据结构哈希表由HashMap来实现,那双向链表这个数据结构是怎么实现的呢?
我们可以定位到LinkedHashMap.Entry类,可以看到该类继承HashMap.Entry类,该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链表。
LinkedHashMap的源码跟踪入口是创建该集合。那我们可以定位到LinkedHashMap的构造方法,该方法实际调用了父类HashMap的相关构造方法来构造一个底层存放的table数组。有兴趣的同学可以往后跟踪,我们把重点放在LinkedHashMap是怎样实现LRU算法的(因为jdk1.8版本修改了HashMap的数据结构,把原来的数组+链表变为了数组+链表[红黑树],引入红黑树是因为链表中数据过多查询慢的问题)。为了方便理解LRU算法,我选择1.8以前的版本作为分析对象。
首先查看put方法,LinkedHashMap的put是调用HashMap的put,但是重写了put方法中的addEntry方法和createEntry方法。在addEntry方法中我们可以看到removeEldestEntry方法,我们就知道了我们重写的removeEldestEntry方法就是在这里被调用。removeEldestEntry为true就会删除链表中的尾节点。源码如下所示:

void addEntry(int hash, K key, V value, int bucketIndex) {  
    // 调用create方法,将新元素以双向链表的的形式加入到映射中。  
    createEntry(hash, key, value, bucketIndex);  
  
    // 删除最近最少使用元素的策略定义  
    Entry<K,V> eldest = header.after;  
    if (removeEldestEntry(eldest)) {  
        removeEntryForKey(eldest.key);  
    } else {  
        if (size >= threshold)  
            resize(2 * table.length);  
    }  
}  

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;  
    // 调用元素的addBrefore方法,将元素加入到哈希、双向链接列表。  
    e.addBefore(header);  
    size++;  
}  

private void addBefore(Entry<K,V> existingEntry) {  
    after  = existingEntry;  
    before = existingEntry.before;  
    before.after = this;  
    after.before = this;  
}

接着我们在看get方法, LinkedHashMap重写了父类HashMap的get方法,实际在调用HashMap.getEntry()方法取得查找的元素后,再根据排序模式(accessOrder)把get方法获取到的值放入链表的表头。在这里我们聊一下排序模式accessOrder,当排序模式accessOrder为true时,记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除,我们可以知道get方法会把经常访问的数据放在了链表的前面,那么不常访问的数据就放在了后面。另一方面我们聊聊排序模式,当排序模式accessOrder为false时,记录插入顺序。通过LinkedHashMap的构造函数accessOrder=false,说明默认指定排序模式为插入顺序。如果你想构造一个LinkedHashMap,并打算按从近期访问最少到近期访问最多的顺序(即访问顺序)来保存元素,可以指定
accessOrder=true。get方法的源码如下:

public V get(Object key) {  
    // 调用父类HashMap的getEntry()方法,取得要查找的元素。  
    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;  
    // 如果定义了LinkedHashMap的迭代顺序为访问顺序,  
    // 则删除以前位置上的元素,并将最新访问的元素添加到链表表头。  
    if (lm.accessOrder) {  
        lm.modCount++;  
        remove();  
        addBefore(lm.header);  
    }  
}


总结:LinkedHashMap集合中的数据间都是有序的,根据其排序模式accessOrder我们可以在集合的put方法中控制哪些元素是需要删除掉的。
(2)LIRS
LRU算法在大多数情况下表现是不错的,但有一个问题:假如某一个查询做了一次全表扫描,将导致缓冲池中的大量数据(可能包含很多很快被访问的热点数据)被替换,从而污染缓冲池。现代数据库一般采用LIRS算法,将缓冲池分为两级,数据首先进入第一级,如果数据在较短的时间内被访问两次或者以上,则成为热点数据进入第二级,每一级内部还是采用LRU替换算法。Oracle数据库中的Touch Count算法和MySQL InnoDB中的替换算法都采用了类似的分级思想。以MySQL InnoDB为例,InnoDB内部的LRU链表分为两部分:新子链表(new sublist)和老子链表(old sublist),默认情况下,前者占5/8,后者占3/8。页面首先插入到老子链表,InnoDB要求页面在老子链表停留时间超过一定值,比如1秒,才有可能被转移到新子链表。当出现全表扫描时,InnoDB将数据页面载入到老子链表,由于数据页面在老子链表中的停留时间不够,不会被转移到新子链表中,这就避免了新子链表中的页面被替换出去的情况。

文章来源:http://book.51cto.com/art/201309/410571.htm

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值