• LinkedHashMap实现LRU算法以及源码解读


    参考:

    https://www.cnblogs.com/jiaoyiping/p/10604463.html

    https://www.cnblogs.com/mengheng/p/3683137.html

    如何使用LinkedHashMap来实现一个LruCache

    最近在看mybatis的源代码,发现了mybatis中实现的LruCache使用到了LinkedHashMap,所以就探究了一下LinkedHashMap是如何支持Lru缓存的

    LinkedHashMap内部维护了一个所有的Entity的双向链表

    同时构造方法可以设置Iterator的时候,是按照插入的顺序排序还是按照访问的顺序排序

    默认是按照插入的顺序来排序的,在构造方法里边可以设置按照访问的顺序来排序

    那究竟按照访问的顺序来排序是什么意思呢?

    LinkedHashMap的get(key)方法是自己实现的,并没有从HashMap里边继承,我们看看get(Key)方法的实现是什么样子的

    我们看afterNodeAccess()方法是如何实现的

    这个方法主要就是移动双向链表的指针,将传入的结点移动到LinkedHashMap维护的双向链表的末尾,这样每次通过get(key)方法访问一个元素,这个元素就会被移动到双向链表的末尾,按照访问的顺序来排序,就是每次通过Iterator来遍历keySet或者是EntrySet的时候,访问过的元素会出现在最后边(因为LinedHashMap的Iterator遍历的时候,遍历的是内部的双向链表,从头结点,遍历到尾结点)

    顺着这样的思路,如果在满足一定条件的情况下,移除掉双向链表的头结点,这样就实现了一个LruCahe

    其实LinkedHashMap已经为我们提供了这样的方法,LinkedHashMap中有一个方法removeEldestEntry(entry) 我们只需要覆盖这个方法,根据我们自己的需求在一定条件下返回true,这样就实现了LruCache
    改方法的默认实现是返回false
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
    }

    LinkedHashMap的afterNodeInsertion()方法会根据其他条件以及removeEldestEntry的返回值来决定是否删除到双向链表的表头元素

    依据此,我们使用LinkedHashMap来实现一个最简单的Lru缓存如下:
    import org.junit.Test;

    import java.util.LinkedHashMap;
    import java.util.Map;
    public class TestCache {
        @Test
        public void testLinkedHashMap() {
            LinkedHashMap<String, String> map = new LinkedHashMap<String, String>(5, 0.75F, true) {
                @Override
                protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                    //当LinkHashMap的容量大于等于5的时候,再插入就移除旧的元素
                    return this.size() >= 5;
                }
            };
            map.put("aa", "bb");
            map.put("cc", "dd");
            map.put("ee", "ff");
            map.put("gg", "hh");
            print(map);
            map.get("cc");
            System.out.println("===================================");
            print(map);
    
            map.get("ee");
            map.get("aa");
            System.out.println("====================================");
            map.put("ss","oo");
            print(map);
        }
    
        void print(LinkedHashMap<String, String> source) {
            source.keySet().iterator().forEachRemaining(System.out::println);
        }
    }   
    

    Mybatis中的Lrucache实现也是类似的思路,比较简单,下边是关键的代码:

    构造方法中调用了setSize()方法,默认缓存1024个元素

    public LruCache(Cache delegate) {
        this.delegate = delegate;
        setSize(1024);
      }
    

    setSize()方法中初始化了HashMap,并实现了removeEldestEntry()方法

    public void setSize(final int size) {
        keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
          private static final long serialVersionUID = 4267176411845948333L;
    
          @Override
          protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
            boolean tooBig = size() > size;
            if (tooBig) {
              eldestKey = eldest.getKey();
            }
            return tooBig;
          }
        };
      }











    LinkedHashMap实现LRU算法

    LinkedHashMap特别有意思,它不仅仅是在HashMap上增加Entry的双向链接,它更能借助此特性实现保证Iterator迭代按照插入顺序(以insert模式创建LinkedHashMap)或者实现LRU(Least Recently Used最近最少算法,以access模式创建LinkedHashMap)。

    下面是LinkedHashMap的get方法的代码

    public V get(Object key) {
            Entry<K,V> e = (Entry<K,V>)getEntry(key);
            if (e == null)
                return null;
            e.recordAccess(this);
            return e.value;
        }

    其中有一段:e.recordAccess(this)。下面我们进入Entry的定义

    void recordAccess(HashMap<K,V> m) {
                LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
                if (lm.accessOrder) {
                    lm.modCount++;
                    remove();
                    addBefore(lm.header);
                }
            }

    这里的addBefore(lm.header)是做什么呢?再看

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

    从这里可以看到了,addBefore(lm.header)是把当前访问的元素挪到head的前面,即最近访问的元素被放到了链表头,如此要实现LRU算法只需要从链表末尾往前删除就可以了,多么巧妙的方法。

    在看到LinkedHashMap之前,我以为实现LRU算法是在每个元素内部维护一个计数器,访问一次自增一次,计数器最小的会被移除。但是要想到,每次add的时候都需要做这么一次遍历循环,并取出最小的抛弃,在HashMap较大的时候效率很差。当然也有其他方法来改进,比如建立<访问次数,LinkedHashMap元素的key>这样的TreeMap,在add的时候往TreeMap里也插入一份,删除的时候取最小的即可,改进了效率但没有LinkedHashMap内部的默认实现来的简捷。

    LinkedHashMap是什么时候删除的呢?

     void addEntry(int hash, K key, V value, int bucketIndex) {
            super.addEntry(hash, key, value, bucketIndex);
     
            // Remove eldest entry if instructed
            Entry<K,V> eldest = header.after;
            if (removeEldestEntry(eldest)) {
                removeEntryForKey(eldest.key);
            }
        }

    在增加Entry的时候,通过removeEldestEntry(eldest)判断是否需要删除最老的Entry,如果需要则remove。注意看这里Entry<K,V> eldest=header.after,记得我们前面提过LinkedHashMap还维护一个双向链表,这里的header.after就是链表尾部最后一个元素(头部元素是head.before)。

    LinkedHashMap默认的removeEldestEntry方法如下

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
            return false;
        }
    总是返回false,所以开发者需要实现LRU算法只需要继承LinkedHashMap并重写removeEldestEntry方法,下面以MyBatis的LRU算法的实现举例
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
          private static final long serialVersionUID = 4267176411845948333L;
     
          protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
            boolean tooBig = size() > size;
            if (tooBig) {
              eldestKey = eldest.getKey();
            }
            return tooBig;
          }
        };

    开发者的子类并不需要直接操作eldest(上例中获得eldestKey只是MyBatis需要映射到Cache对象中的元素),只要根据自己的条件(一般是元素个数是否到达阈值)返回true/false即可。注意,要按照LRU排序必须在new LinkedHashMap()的构造函数的最后一个参数传入true(true代表LinkedHashMap内部的双向链表按访问顺序排序,false代表按插入顺序排序)。

    在LinkedHashMap的注释里明确提到,该类在保持插入顺序、不想HashMap那样混乱的情况下,又没有像TreeMap那样的性能损耗。同时又能够很巧妙地实现LRU算法。其他方面和HashMap功能一致。有兴趣的同学可以仔细看看LinkedHashMap的实现。





    附录:
    LinkedHashMap的几种遍历方式
    import java.util.Iterator;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    public class LinkedHashMapIteratorDemo {
    
        public static void main(String[] args) {
            LinkedHashMap<String,String> linkedHashMap = new LinkedHashMap<>();
            linkedHashMap.put("a","aa");
            linkedHashMap.put("b","bb");
            linkedHashMap.put("c","cc");
    
            Iterator iterator=linkedHashMap.entrySet().iterator();
            while (iterator.hasNext()){
                Map.Entry entry = (Map.Entry) iterator.next();
                System.out.println(entry.getKey()+" : "+entry.getValue());
            }
    
            System.out.println("----------------------");
            for(Map.Entry<String,String>entry: linkedHashMap.entrySet()){
                System.out.println(entry.getKey()+" : "+entry.getValue());
            }
    
            System.out.println("----------------------");
            System.out.println(linkedHashMap.keySet());
            System.out.println(linkedHashMap.values());
            System.out.println("----------------------");
    
            for (String key : linkedHashMap.keySet()){
                System.out.println(key+" : "+linkedHashMap.get(key));
            }
    
        }
    
    }

    执行结果:

    a : aa
    b : bb
    c : cc
    ----------------------
    a : aa
    b : bb
    c : cc
    ----------------------
    [a, b, c]
    [aa, bb, cc]
    ----------------------
    a : aa
    b : bb
    c : cc









  • 相关阅读:
    P4890 Never·island
    P2617 Dynamic Rankings
    P3243 [HNOI2015]菜肴制作
    P4172 [WC2006]水管局长
    P4219 [BJOI2014]大融合
    P5241 序列
    P1501 [国家集训队]Tree II
    无法读取用户配置文件,系统自动建立Temp临时用户
    组件服务 控制台打不开
    打印服务器 功能地址保护错误
  • 原文地址:https://www.cnblogs.com/xuwc/p/13957717.html
Copyright © 2020-2023  润新知