美文网首页
LRU子与LinkedHashMap美的日常

LRU子与LinkedHashMap美的日常

作者: 我永远喜欢御坂美琴 | 来源:发表于2020-07-12 18:42 被阅读0次

    一、LRU

    1. LRU原理

    LRU--Least Recent Used,在操作系统课程的虚拟内存章节曾经学过,当内存空间不够时需要对内存中的页进行淘汰。LRU即为一种淘汰策略,即淘汰掉最不经常使用的。

    以下引用LRU原理和Redis实现——一个今日头条的面试题的图片简单说一下LRU的原理。

    假设内存大小只能容纳三个页的大小,其中访问依照7 0 1 2 0 3 0 4的顺序访问,位于最上方的页即为最经常访问的,处于最下方的页为最不经常访问的。

    2. LRU实现

    我们可以通过使用HashMap和双向链表实现LRU

    image

    利用HashMap能在常数时间内定位到节点,并且可以通过双向链表访问相邻节点。若需要淘汰节点,可在常数时间内定位到尾部节点并移除;若需要插入或者访问节点,可在常数时间内将节点移至头部。

    二、LinkedHashMap

    LinkedHashMap的数据结构即为上文介绍LRU实现所采用的数据结构,并且实现了Map接口还继承了HashMap,有关于HashMap的知识点此处不会过多展开,可移步至我另一篇介绍HashMap的文章:我的HashMap果然有问题

    1. LinkedHashMap源码

    public class LinkedHashMap<K,V>
        extends HashMap<K,V>
        implements Map<K,V>
    {
        static class Entry<K,V> extends HashMap.Node<K,V> {
            Entry<K,V> before, after;
            Entry(int hash, K key, V value, Node<K,V> next) {
                super(hash, key, value, next);
            }
        }
    
        /**
         * The head (eldest) of the doubly linked list.
         */
        transient LinkedHashMap.Entry<K,V> head;
    
        /**
         * The tail (youngest) of the doubly linked list.
         */
        transient LinkedHashMap.Entry<K,V> tail;
    
        /**
         * The iteration ordering method for this linked hash map: <tt>true</tt>
         * for access-order, <tt>false</tt> for insertion-order.
         */
        final boolean accessOrder;
    
        ...
    
        /**
         * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
         * with the specified initial capacity and load factor.
         *
         * @param  initialCapacity the initial capacity
         * @param  loadFactor      the load factor
         * @throws IllegalArgumentException if the initial capacity is negative
         *         or the load factor is nonpositive
         */
        public LinkedHashMap(int initialCapacity, float loadFactor) {
            super(initialCapacity, loadFactor);
            accessOrder = false;
        }
    
        /**
         * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
         * with the specified initial capacity and a default load factor (0.75).
         *
         * @param  initialCapacity the initial capacity
         * @throws IllegalArgumentException if the initial capacity is negative
         */
        public LinkedHashMap(int initialCapacity) {
            super(initialCapacity);
            accessOrder = false;
        }
    
        /**
         * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
         * with the default initial capacity (16) and load factor (0.75).
         */
        public LinkedHashMap() {
            super();
            accessOrder = false;
        }
    
        /**
         * Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with
         * the same mappings as the specified map.  The <tt>LinkedHashMap</tt>
         * instance is created with a default load factor (0.75) and an initial
         * capacity sufficient to hold the mappings in the specified map.
         *
         * @param  m the map whose mappings are to be placed in this map
         * @throws NullPointerException if the specified map is null
         */
        public LinkedHashMap(Map<? extends K, ? extends V> m) {
            super();
            accessOrder = false;
            putMapEntries(m, false);
        }
    
        /**
         * Constructs an empty <tt>LinkedHashMap</tt> instance with the
         * specified initial capacity, load factor and ordering mode.
         *
         * @param  initialCapacity the initial capacity
         * @param  loadFactor      the load factor
         * @param  accessOrder     the ordering mode - <tt>true</tt> for
         *         access-order, <tt>false</tt> for insertion-order
         * @throws IllegalArgumentException if the initial capacity is negative
         *         or the load factor is nonpositive
         */
        public LinkedHashMap(int initialCapacity,
                             float loadFactor,
                             boolean accessOrder) {
            super(initialCapacity, loadFactor);
            this.accessOrder = accessOrder;
        }
        
        ...
    
    

    其中值得注意的就是accessOrder属性,若accessOrder为false则代表插入顺序,若为true则代表访问顺序。说人话就是在LinkedHashMap中维护的双向链表的顺序,当插入元素时LinkedHashMap会将所插入的元素插入双向链表表尾的位置,而当accessOrder为true时对其中的元素进行访问时,LinkedHashMap会将访问的元素移至双向链表最后端,反之则不会这么做。在LinkedHashMap的put和get方法中我们可以很清楚的看到。

    LinkedHashMap的put方法并没有实现,使用的put方法为HashMap的put方法,此处不做介绍。但在HashMap的putVal源码中我们可以看到

    这两个方法在HashMap中为空方法,在LinkedHashMap中对这两个方法进行了重写。

        //有可能会删除最不常访问的节点,即LinkedHashMap实现LRU的关键方法之一,后面会说
        void afterNodeInsertion(boolean evict) { // possibly remove eldest
            LinkedHashMap.Entry<K,V> first;
            if (evict && (first = head) != null && removeEldestEntry(first)) {
                K key = first.key;
                removeNode(hash(key), key, null, false, true);
            }
        }
        //将节点移至链表最后端
        void afterNodeAccess(Node<K,V> e) { // move node to last
            LinkedHashMap.Entry<K,V> last;
            if (accessOrder && (last = tail) != e) {
                LinkedHashMap.Entry<K,V> p =
                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
                p.after = null;
                if (b == null)
                    head = a;
                else
                    b.after = a;
                if (a != null)
                    a.before = b;
                else
                    last = b;
                if (last == null)
                    head = p;
                else {
                    p.before = last;
                    last.after = p;
                }
                tail = p;
                ++modCount;
            }
        }
    

    LinkedHashMap还对HashMap.Node类进行重写,其中在newNode和newTreeNode方法中实现了对链表的插入。我看到这里不禁感叹多态的强大和灵活性,真的太强了。

        Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
            LinkedHashMap.Entry<K,V> p =
                new LinkedHashMap.Entry<K,V>(hash, key, value, e);
            linkNodeLast(p);
            return p;
        }
    
        TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
            TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
            linkNodeLast(p);
            return p;
        }
    

    接下来是get方法

        public V get(Object key) {
            Node<K,V> e;
            //getNode继承自HashMap
            if ((e = getNode(hash(key), key)) == null)
                return null;
            //对accessOrder进行了判断,若为true则将节点移至尾部
            if (accessOrder)
                afterNodeAccess(e);
            return e.value;
        }
    
        //将节点移至链表尾部
        void afterNodeAccess(Node<K,V> e) { // move node to last
            LinkedHashMap.Entry<K,V> last;
            if (accessOrder && (last = tail) != e) {
                LinkedHashMap.Entry<K,V> p =
                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
                p.after = null;
                if (b == null)
                    head = a;
                else
                    b.after = a;
                if (a != null)
                    a.before = b;
                else
                    last = b;
                if (last == null)
                    head = p;
                else {
                    p.before = last;
                    last.after = p;
                }
                tail = p;
                ++modCount;
            }
        }
    

    2. LinkedHashMap实现LRU

    现在我们回过头来看看afterNodeInsertion方法。

        //有可能会删除最不常访问的节点,即LinkedHashMap实现LRU的关键方法之一
        //@param evict if false, the table is in creation mode.
        //当调用put方法时,evict恒为true
        void afterNodeInsertion(boolean evict) { // possibly remove eldest
            LinkedHashMap.Entry<K,V> first;、
            //其中对头节点(也为待删除节点)first进行removeEldestEntry(first)的判断
            if (evict && (first = head) != null && removeEldestEntry(first)) {
                K key = first.key;
                removeNode(hash(key), key, null, false, true);
            }
        }
    
        protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
            return false;
        }
    

    因为LinkedHashMap的removeEldestEntry方法恒返回false,故永远无法触发删除头节点操作。但看到protected会想到什么?嘿嘿嘿,protected不就是给我们重写用的?接下来我将重写removeEldestEntry方法实现一个基于LinkedHashMap的简单LRU。

        public class MyLRU extends LinkedHashMap {
    
    //容量    
    private int mysize;
    
        public MyLRU(int size) {
            super(16, 0.75f, true);
            this.mysize = size;
        }
    
        @Override
        protected boolean removeEldestEntry(Map.Entry eldest) {
            return size() > mysize;
        }
    
        public static void main(String[] args) {
            Map map = new MyLRU(3);
            for (int i = 0; i < 5; i++) {
                map.put(i, i);
                if (i == 2) map.get(0);
            }
            System.out.println("size: " + map.size());
    
            Iterator<Map.Entry> iterator=map.entrySet().iterator();
            while (iterator.hasNext()){
                Map.Entry entry=iterator.next();
                System.out.print(entry.getKey());
                System.out.print("  ");
                System.out.println(entry.getValue());
            }
    
        }
    }
    

    大功告成!


    下面是私货时间



    参考文献:

    LinkedHashMap就这么简单【源码剖析】

    LRU原理和Redis实现——一个今日头条的面试题

    相关文章

      网友评论

          本文标题:LRU子与LinkedHashMap美的日常

          本文链接:https://www.haomeiwen.com/subject/qerncktx.html