本篇将描述如何使用LinkedHashMap实现LRU以及FIFO缓存,并将从LinkedHashMap源码层面描述是如何实现这两种缓存的。
1.缓存描述
首先介绍一下FIFO、LRU两种缓存:
FIFO(First In First out):先见先出,淘汰最先近来的页面,新进来的页面最迟被淘汰,完全符合队列。
LRU(Least recently used):最近最少使用,淘汰最近不使用的页面。
2.代码实现
以下是通过LinkedHashMap实现两种缓存。
public class Cache{ private LinkedHashMap map = null;//用LinkedHashMap实现 private int cap;//上限 public Cache(int cap, boolean lru) {//构造函数(lru:false即为lru) this.cap = cap; map = new LinkedHashMap (cap, 0.75f, lru){//第三个参数即为LinkedHashMap中的accessOrder true:将按照访问顺序(如果已经存在将其插入末尾); false:按照插入数序(再次插入不影响顺序) @Override protected boolean removeEldestEntry(Map.Entry eldest) {//重写删除最早的entry return size() > cap;//如果条件当前size大于cap,就删除最早的(返回true) } }; } public V put(K key, V value){ return map.put(key, value); } @Override public String toString() { return map.toString(); } }
重写toString方法是为了测试时可以更简单的打印出两种缓存的效果。
下面是测试方法:
public static void main(String[] args) { //lru测试 Cachecache = new Cache (3, true); cache.put(1, 1); System.out.println(cache); cache.put(2, 2); System.out.println(cache); cache.put(3, 3); System.out.println(cache); cache.put(1, 1); System.out.println(cache); cache.put(4, 4); System.out.println(cache); //fifo测试 cache = new Cache (3, false); cache.put(1, 1); System.out.println(cache); cache.put(2, 2); System.out.println(cache); cache.put(3, 3); System.out.println(cache); cache.put(1, 1); System.out.println(cache); cache.put(4, 4); System.out.println(cache); }
以下是测试结果:
可以看到lru模式下,当缓存被访问到时,会将其放到末尾,因此按照最近最少被使用淘汰缓存;而fifo模式下缓存顺序按照进入顺序,最先进来的最先被淘汰。
那么为什么如此简单的使用LinkedHashMap就可以完成这两种常用缓存淘汰策略的实现呢?下面我们从LinkedHashMap源码来了解其内部是如何工作的。
3.源码分析
首先,我们来说说LinkedHashMap是如何做到能记录插入顺序的。这就要看它的Entry节点了:
1 static class Entryextends HashMap.Node { 2 Entry before, after; 3 Entry(int hash, K key, V value, Node next) { 4 super(hash, key, value, next); 5 } 6 } 7 8 transient LinkedHashMap.Entry head; 9 10 transient LinkedHashMap.Entry tail;
LinkedHashMap中的Entry继承了HashMap中的Node,并增加了一个before和after指向插入的前(后)一个Entry,并在LinkedHashMap中用head和tail记录首位节点,这个结构看起来就像是把HashMap和LinkedList结合起来,做到记录插入顺序。
那么LinkedHashMap中的put方法呢?当你打开LinkedHashMap去查找put方法时,你会发现找不到,因为其继承了HashMap,因此直接使用HashMap中的put方法,那么同一个put方法是如何做到在插入之后将before,after指针更新的呢?来看看HashMap的put是如何设计的吧:
1 public V put(K key, V value) { 2 return putVal(hash(key), key, value, false, true); 3 } 4 5 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 6 boolean evict) { 7 Node[] tab; Node p; int n, i; 8 if ((tab = table) == null || (n = tab.length) == 0) 9 n = (tab = resize()).length; 10 if ((p = tab[i = (n - 1) & hash]) == null) 11 tab[i] = newNode(hash, key, value, null); 12 else { 13 Node e; K k; 14 if (p.hash == hash && 15 ((k = p.key) == key || (key != null && key.equals(k)))) 16 e = p; 17 else if (p instanceof TreeNode) 18 e = ((TreeNode )p).putTreeVal(this, tab, hash, key, value); 19 else { 20 for (int binCount = 0; ; ++binCount) { 21 if ((e = p.next) == null) { 22 p.next = newNode(hash, key, value, null); 23 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 24 treeifyBin(tab, hash); 25 break; 26 } 27 if (e.hash == hash && 28 ((k = e.key) == key || (key != null && key.equals(k)))) 29 break; 30 p = e; 31 } 32 } 33 if (e != null) { // existing mapping for key 34 V oldValue = e.value; 35 if (!onlyIfAbsent || oldValue == null) 36 e.value = value; 37 afterNodeAccess(e); 38 return oldValue; 39 } 40 } 41 ++modCount; 42 if (++size > threshold) 43 resize(); 44 afterNodeInsertion(evict); 45 return null; 46 }
我们着重看第37行以及44行,当插入一个节点的key存在于map中时,会调用37行的afterNodeAccess,当不存在时(即插入一个新节点),会调用44行的afterNodeInsertion。LinkedHashMap的before和after节点设置以及head和tail节点更新也是在这里完成的。
我们进入LinkedHashMap中的这两个方法看看其中做了些什么。首先来看afterNodeAccess:
1 void afterNodeAccess(Nodee) { // move node to last 2 LinkedHashMap.Entry last; 3 if (accessOrder && (last = tail) != e) { 4 LinkedHashMap.Entry p = 5 (LinkedHashMap.Entry )e, b = p.before, a = p.after; 6 p.after = null; 7 if (b == null) 8 head = a; 9 else 10 b.after = a; 11 if (a != null) 12 a.before = b; 13 else 14 last = b; 15 if (last == null) 16 head = p; 17 else { 18 p.before = last; 19 last.after = p; 20 } 21 tail = p; 22 ++modCount; 23 } 24 }
上面的代码判断了accessOrder是否为true,如果是,则为根据访问数序,进入下面的代码块,将p放到tail(最新的位置),这个语意与lru一致;如果不是,就什么都不干,本方法完成,这个语意与fifo一致,因此Cache中构造方法的lru与accessOrder是一致的。
那么afterNodeInsertion又做了什么呢?
1 void afterNodeInsertion(boolean evict) { // possibly remove eldest 2 LinkedHashMap.Entryfirst; 3 if (evict && (first = head) != null && removeEldestEntry(first)) { 4 K key = first.key; 5 removeNode(hash(key), key, null, false, true); 6 } 7 } 8 9 //这个方法是例子中Cache中的实现,当前(插入后)的size大于容量,返回true 10 protected boolean removeEldestEntry(Map.Entry eldest) { 11 return size() > cap; 12 }
前7行是afterNodeInsertion内部的实现,判断是否需要删除最老的元素,如果是,就把最老的元素(最先进入的head节点)删除,而需要开发者实现的removeEldestEntry只需要判断size是否大于容量即可,如果大于,就说明缓存容量不够了,要删除head。而通过afterNodeAccess方法实现的lru中head为最久未被使用的元素,fifo中的head为最先进入的元素。
相信看到这里,你对如何使用LinkedHashMap实现LRU和FIFO两种缓存置换算法以及其原理都了解了吧,那么自己尝试动手做一下吧。
(tips:可以看看springboot、hibernate中如何用LinkedHashMap实现的lru缓存哦!)