(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将数据页面载入到老子链表,由于数据页面在老子链表中的停留时间不够,不会被转移到新子链表中,这就避免了新子链表中的页面被替换出去的情况。