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

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

1.缓存描述

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

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

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

2.代码实现

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

public class Cache<K,V>{

    private LinkedHashMap<K, V> map = null;//用LinkedHashMap实现
    private int cap;//上限

    public Cache(int cap, boolean lru) {//构造函数(lru:false即为lru)
        this.cap = cap;
        map = new LinkedHashMap<K,V>(cap, 0.75f, lru){//第三个参数即为LinkedHashMap中的accessOrder true:将按照访问顺序(如果已经存在将其插入末尾); false:按照插入数序(再次插入不影响顺序)
            @Override
            protected boolean removeEldestEntry(Map.Entry<K,V> 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测试
        Cache<Integer, Integer> cache = new Cache<Integer, Integer>(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<Integer, Integer>(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 Entry<K,V> extends HashMap.Node<K,V> {
 2         Entry<K,V> before, after;
 3         Entry(int hash, K key, V value, Node<K,V> next) {
 4             super(hash, key, value, next);
 5         }
 6     }
 7
 8     transient LinkedHashMap.Entry<K,V> head;
 9
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     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<K,V>[] tab; Node<K,V> 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<K,V> 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<K,V>)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(Node<K,V> e) { // move node to last
 2         LinkedHashMap.Entry<K,V> last;
 3         if (accessOrder && (last = tail) != e) {
 4             LinkedHashMap.Entry<K,V> p =
 5                 (LinkedHashMap.Entry<K,V>)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.Entry<K,V> first;
 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<K,V> 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缓存哦!)

原文地址:https://www.cnblogs.com/zzzdp/p/9417206.html

时间: 2024-10-29 19:09:47

LRU、FIFO缓存实现以及LinkedHashMap源码的相关文章

LRU算法实现,HashMap与LinkedHashMap源码的部分总结

关于HashMap与LinkedHashMap源码的一些总结 JDK1.8之后的HashMap底层结构中,在数组(Node<K,V> table)长度大于64的时候且链表(依然是Node)长度大于8的时候,链表在转换为红黑树时,链表长度小于等于6时将不会进行转化为红黑树.目的是为了保证效率.其中链表的结点只有next,LinkedHashMap是在Entry<K,V>中添加before, after(双向链表的定义),保证可迭代,遍历时为存入顺序. 下面是LinkedHashMap

【源码】LinkedHashMap源码剖析

注:以下源码基于jdk1.7.0_11 之前的两篇文章通过源码分析了两种常见的Map集合,HashMap和Hashtable.本文将继续介绍另一种Map集合--LinkedHashMap. 顾名思义,LinkedHashMap除了是一个HashMap之外,还带有LinkedList的特点,也就是说能够保持遍历的顺序和插入的顺序一致,那么它是怎么做到的呢?下面我们开始分析. 首先看构造器. public class LinkedHashMap<K,V> extends HashMap<K,

【Java集合源码剖析】LinkedHashmap源码剖析

LinkedHashMap简介 LinkedHashMap是HashMap的子类,与HashMap有着同样的存储结构,但它加入了一个双向链表的头结点,将所有put到LinkedHashmap的节点一一串成了一个双向循环链表,因此它保留了节点插入的顺序,可以使节点的输出顺序与输入顺序相同. LinkedHashMap可以用来实现LRU算法(这会在下面的源码中进行分析). LinkedHashMap同样是非线程安全的,只在单线程环境下使用. LinkedHashMap源码剖析 LinkedHashM

转:【Java集合源码剖析】LinkedHashmap源码剖析

转载请注明出处:http://blog.csdn.net/ns_code/article/details/37867985   前言:有网友建议分析下LinkedHashMap的源码,于是花了一晚上时间研究了下,分享出此文(这个系列的最后一篇博文了),希望大家相互学习.LinkedHashMap的源码理解起来也不难(当然,要建立在对HashMap源码有较好理解的基础上). LinkedHashMap简介 LinkedHashMap是HashMap的子类,与HashMap有着同样的存储结构,但它加

LinkedHashMap 源码分析

LinkedHashMap 源码分析 1. 基本结构 1. 实现 实现的接口是 Map 2. 继承 ?? 继承的是 HashMap 这个就比较熟悉了,事实上我们会看到 LinkedHashMap 代码量非常的少,主要就是因为他继承的 HashMap ,继承了大多数的操作. 仔细一点的都会发现 HashMap 里面有非常多的空白方法,这些方法其实是模板方法,为了让继承 HashMap 的类重写一些自己的特性.而不破坏代码结构. 3. 数据域 1. 基本字段 ?? 在 HashMap 的基础上他添加

分析LinkedHashMap源码的LRU实现

一.前言 前段时间研究了memcached,而且操作系统的课程也刚刚完成,在两个里面多次出现LRU(last recently used最近最少使用)算法,虽然思想很简单.但是还是值得我们研究,无意间在看LinkedHashMap的源码的时候看见貌似这个类里面有默认的LRU实现.我们现在就来分析一下他的源代码 /** * Returns <tt>true</tt> if this map should remove its eldest entry. * This method i

LinkedHashMap源码及LRU算法应用

先来说说它的特点,然后在一一通过分析源码来验证其实现原理 1.能够保证插入元素的顺序.深入一点讲,有两种迭代元素的方式,一种是按照插入元素时的顺序迭代,比如,插入A,B,C,那么迭代也是A,B,C,另一种是按照访问顺序,比如,在迭代前,访问了B,那么迭代的顺序就是A,C,B,比如在迭代前,访问了B,接着又访问了A,那么迭代顺序为C,B,A,比如,在迭代前访问了B,接着又访问了B,然后在访问了A,迭代顺序还是C,B,A.要说明的意思就是不是近期访问的次数最多,就放最后面迭代,而是看迭代前被访问的时

缓存框架OSCache部分源码分析

在并发量比较大的场景,如果采用直接访问数据库的方式,将会对数据库带来巨大的压力,严重的情况下可能会导致数据库不可用状态,并且时间的消耗也是不能容忍的.在这种情况下,一般采用缓存的方式.将经常访问的热点数据提前加载到内存中,这样能够大大降低数据库的压力. OSCache是一个开源的缓存框架,虽然现在已经停止维护了,但是对于OSCache的实现还是值得学习和借鉴的.下面通过OSCache的部分源码分析OSCache的设计思想. 缓存数据结构 通常缓存都是通过<K,V>这种数据结构存储,但缓存都是应

LinkedHashMap源码

前面分析了HashMap的实现,我们知道其底层数据存储是一个hash表(数组+单向链表).接下来我们看一下另一个LinkedHashMap,它是HashMap的一个子类,他在HashMap的基础上维持了一个双向链表(hash表+双向链表),在遍历的时候可以使用插入顺序(先进先出,类似于FIFO),或者是最近最少使用(LRU)的顺序. 来具体看下LinkedHashMap的实现. 1.定义  1 2 3 public class LinkedHashMap<K,V>     extends Ha