LRU、FIFO缓存实现以及LinkedHashMap源码

蔚落 2021-05-27 20:38 561阅读 0赞

  本篇将描述如何使用LinkedHashMap实现LRU以及FIFO缓存,并将从LinkedHashMap源码层面描述是如何实现这两种缓存的。

1.缓存描述

  首先介绍一下FIFO、LRU两种缓存:

    FIFO(First In First out):先见先出,淘汰最先近来的页面,新进来的页面最迟被淘汰,完全符合队列。

    LRU(Least recently used):最近最少使用,淘汰最近不使用的页面。

2.代码实现

  以下是通过LinkedHashMap实现两种缓存。

复制代码

  1. public class Cache<K,V>{
  2. private LinkedHashMap<K, V> map = null;//用LinkedHashMap实现
  3. private int cap;//上限
  4. public Cache(int cap, boolean lru) {//构造函数(lru:false即为lru)
  5. this.cap = cap;
  6. map = new LinkedHashMap<K,V>(cap, 0.75f, lru){//第三个参数即为LinkedHashMap中的accessOrder true:将按照访问顺序(如果已经存在将其插入末尾); false:按照插入数序(再次插入不影响顺序)
  7. @Override
  8. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {//重写删除最早的entry
  9. return size() > cap;//如果条件当前size大于cap,就删除最早的(返回true)
  10. }
  11. };
  12. }
  13. public V put(K key, V value){
  14. return map.put(key, value);
  15. }
  16. @Override
  17. public String toString() {
  18. return map.toString();
  19. }
  20. }

复制代码

  重写toString方法是为了测试时可以更简单的打印出两种缓存的效果。

  下面是测试方法:

复制代码

  1. public static void main(String[] args) {
  2. //lru测试
  3. Cache<Integer, Integer> cache = new Cache<Integer, Integer>(3, true);
  4. cache.put(1, 1);
  5. System.out.println(cache);
  6. cache.put(2, 2);
  7. System.out.println(cache);
  8. cache.put(3, 3);
  9. System.out.println(cache);
  10. cache.put(1, 1);
  11. System.out.println(cache);
  12. cache.put(4, 4);
  13. System.out.println(cache);
  14. //fifo测试
  15. cache = new Cache<Integer, Integer>(3, false);
  16. cache.put(1, 1);
  17. System.out.println(cache);
  18. cache.put(2, 2);
  19. System.out.println(cache);
  20. cache.put(3, 3);
  21. System.out.println(cache);
  22. cache.put(1, 1);
  23. System.out.println(cache);
  24. cache.put(4, 4);
  25. System.out.println(cache);
  26. }

复制代码

  以下是测试结果:

format_png

  可以看到lru模式下,当缓存被访问到时,会将其放到末尾,因此按照最近最少被使用淘汰缓存;而fifo模式下缓存顺序按照进入顺序,最先进来的最先被淘汰。

  那么为什么如此简单的使用LinkedHashMap就可以完成这两种常用缓存淘汰策略的实现呢?下面我们从LinkedHashMap源码来了解其内部是如何工作的。

3.源码分析

  首先,我们来说说LinkedHashMap是如何做到能记录插入顺序的。这就要看它的Entry节点了:

复制代码

  1. 1 static class Entry<K,V> extends HashMap.Node<K,V> {
  2. 2 Entry<K,V> before, after;
  3. 3 Entry(int hash, K key, V value, Node<K,V> next) {
  4. 4 super(hash, key, value, next);
  5. 5 }
  6. 6 }
  7. 7
  8. 8 transient LinkedHashMap.Entry<K,V> head;
  9. 9
  10. 10 transient LinkedHashMap.Entry<K,V> 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. 1 public V put(K key, V value) {
  2. 2 return putVal(hash(key), key, value, false, true);
  3. 3 }
  4. 4
  5. 5 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  6. 6 boolean evict) {
  7. 7 Node<K,V>[] tab; Node<K,V> p; int n, i;
  8. 8 if ((tab = table) == null || (n = tab.length) == 0)
  9. 9 n = (tab = resize()).length;
  10. 10 if ((p = tab[i = (n - 1) & hash]) == null)
  11. 11 tab[i] = newNode(hash, key, value, null);
  12. 12 else {
  13. 13 Node<K,V> e; K k;
  14. 14 if (p.hash == hash &&
  15. 15 ((k = p.key) == key || (key != null && key.equals(k))))
  16. 16 e = p;
  17. 17 else if (p instanceof TreeNode)
  18. 18 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  19. 19 else {
  20. 20 for (int binCount = 0; ; ++binCount) {
  21. 21 if ((e = p.next) == null) {
  22. 22 p.next = newNode(hash, key, value, null);
  23. 23 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  24. 24 treeifyBin(tab, hash);
  25. 25 break;
  26. 26 }
  27. 27 if (e.hash == hash &&
  28. 28 ((k = e.key) == key || (key != null && key.equals(k))))
  29. 29 break;
  30. 30 p = e;
  31. 31 }
  32. 32 }
  33. 33 if (e != null) { // existing mapping for key
  34. 34 V oldValue = e.value;
  35. 35 if (!onlyIfAbsent || oldValue == null)
  36. 36 e.value = value;
  37. 37 afterNodeAccess(e);
  38. 38 return oldValue;
  39. 39 }
  40. 40 }
  41. 41 ++modCount;
  42. 42 if (++size > threshold)
  43. 43 resize();
  44. 44 afterNodeInsertion(evict);
  45. 45 return null;
  46. 46 }

复制代码

  我们着重看第37行以及44行,当插入一个节点的key存在于map中时,会调用37行的afterNodeAccess,当不存在时(即插入一个新节点),会调用44行的afterNodeInsertion。LinkedHashMap的before和after节点设置以及head和tail节点更新也是在这里完成的。

  我们进入LinkedHashMap中的这两个方法看看其中做了些什么。首先来看afterNodeAccess:

复制代码

  1. 1 void afterNodeAccess(Node<K,V> e) { // move node to last
  2. 2 LinkedHashMap.Entry<K,V> last;
  3. 3 if (accessOrder && (last = tail) != e) {
  4. 4 LinkedHashMap.Entry<K,V> p =
  5. 5 (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
  6. 6 p.after = null;
  7. 7 if (b == null)
  8. 8 head = a;
  9. 9 else
  10. 10 b.after = a;
  11. 11 if (a != null)
  12. 12 a.before = b;
  13. 13 else
  14. 14 last = b;
  15. 15 if (last == null)
  16. 16 head = p;
  17. 17 else {
  18. 18 p.before = last;
  19. 19 last.after = p;
  20. 20 }
  21. 21 tail = p;
  22. 22 ++modCount;
  23. 23 }
  24. 24 }

复制代码

  上面的代码判断了accessOrder是否为true,如果是,则为根据访问数序,进入下面的代码块,将p放到tail(最新的位置),这个语意与lru一致;如果不是,就什么都不干,本方法完成,这个语意与fifo一致,因此Cache中构造方法的lru与accessOrder是一致的。

  那么afterNodeInsertion又做了什么呢?

复制代码

  1. 1 void afterNodeInsertion(boolean evict) { // possibly remove eldest
  2. 2 LinkedHashMap.Entry<K,V> first;
  3. 3 if (evict && (first = head) != null && removeEldestEntry(first)) {
  4. 4 K key = first.key;
  5. 5 removeNode(hash(key), key, null, false, true);
  6. 6 }
  7. 7 }
  8. 8
  9. 9 //这个方法是例子中Cache中的实现,当前(插入后)的size大于容量,返回true
  10. 10 protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  11. 11 return size() > cap;
  12. 12 }

复制代码

  前7行是afterNodeInsertion内部的实现,判断是否需要删除最老的元素,如果是,就把最老的元素(最先进入的head节点)删除,而需要开发者实现的removeEldestEntry只需要判断size是否大于容量即可,如果大于,就说明缓存容量不够了,要删除head。而通过afterNodeAccess方法实现的lru中head为最久未被使用的元素,fifo中的head为最先进入的元素。

  相信看到这里,你对如何使用LinkedHashMap实现LRU和FIFO两种缓存置换算法以及其原理都了解了吧,那么自己尝试动手做一下吧。

  (tips:可以看看springboot、hibernate中如何用LinkedHashMap实现的lru缓存哦!)

发表评论

表情:
评论列表 (有 0 条评论,561人围观)

还没有评论,来说两句吧...

相关阅读