zoukankan      html  css  js  c++  java
  • 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









  • 相关阅读:
    《应用Yii1.1和PHP5进行敏捷Web开发》学习笔记(转)
    YII 小模块功能
    Netbeans代码配色主题大搜集
    opensuse 启动巨慢 解决方法 90s多
    opensuse 安装 网易云音乐 rpm netease music
    linux qq rpm deb opensuse
    openSUSE 安装 alien
    第一行代码 Android 第2版
    Android Studio AVD 虚拟机 联网 失败
    docker error during connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.29/containers/json: open //./pipe/docker_engine: The system cannot find the file specified. In the default daemon configuratio
  • 原文地址:https://www.cnblogs.com/xuwc/p/13957717.html
Copyright © 2011-2022 走看看