LRUCache原理及HashMap LinkedHashMap内部实现原理

原创 2015年11月27日 17:11:18

LRUCache概述

在开发android应用时,加载图片我们都会加上二级缓存,一级是内存缓存,二级是硬盘缓存。在内存缓存中我们一般采用SDK提供给我们的LRUCahce。
LRU(least recently uses最近最少使用)是一种页面置换算法,最近最少使用的被替换掉。LRU算法是以时间轴为依据进行替换,而不是使用频率为依据替换。
常用的页面置换算法还有:
LFU(least frequently used 最不经常使用)是以单位时间内使用频率为依据进行替换,使用频率低的被替换掉。
FIFO(first in fist out 先进先出)按顺序进行替换。

LRUCache实现原理

先看一下LruCache的构造方法

public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

可以看到LruCache内部维护了一个LinkedHashMap,LinkedHashMap继承自HashMap,所以我们先来研究下HashMap的实现原理。

HashMap的实现原理

还是先看下构造方法

public HashMap(int capacity) {
        if (capacity < 0) {
            throw new IllegalArgumentException("Capacity: " + capacity);
        }

        if (capacity == 0) {
            @SuppressWarnings("unchecked")
            HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;
            table = tab;
            threshold = -1; // Forces first put() to replace EMPTY_TABLE
            return;
        }

        if (capacity < MINIMUM_CAPACITY) {
            capacity = MINIMUM_CAPACITY;
        } else if (capacity > MAXIMUM_CAPACITY) {
            capacity = MAXIMUM_CAPACITY;
        } else {
            capacity = Collections.roundUpToPowerOfTwo(capacity);
        }
        makeTable(capacity);
    }

其中 Collections.roundUpToPowerOfTwo(capacity); 方法是得到一个不小于所给数的2^n的数,就是说假如传入参数10,那么返回不小于10的2的n次方数就是16。具体的可以看这里
看下makeTable 方法

private HashMapEntry<K, V>[] makeTable(int newCapacity) {
        @SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable
                = (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];
        table = newTable;
        threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
        return newTable;
    }

可以看到,HashMap的构造方法中生成了一个大小为2^n的HashMapEntry的数组。我们看下HashMapEntry 的结构

static class HashMapEntry<K, V> implements Entry<K, V> {
        final K key;
        V value;
        final int hash;
        HashMapEntry<K, V> next;

其中有map中的key、value还有根据key得到的hash值和它的后继引用next。其中key value 都是Object
既然是基于Map的,那么我们看下HashMapput方法和get方法:

@Override public V put(K key, V value) {
        if (key == null) {
        //如果key为null调用该方法处理,需要注意的是HashMap允许key和value为null
            return putValueForNullKey(value);
        }
        //根据key得到hash值
        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        //根据hash值和内部维护数组的长度得到索引
        int index = hash & (tab.length - 1);
        //如果index处的e不为null,通过循环不断遍历e的下一个元素
        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
        // 找到指定 key 与需要放入的 key 相等(hash 值相同  
     // 并且key的值相等)说明map中已经存在该数据,那么执行替换
            if (e.hash == hash && key.equals(e.key)) {
                //该方法是空实现,注意LinkedHashMap中会用到
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // No entry for (non-null) key is present; create one
        //index处的Entry为null,说明此处还没有数据Entry
        modCount++;
        if (size++ > threshold) {
        //大于数组的长度,扩容 大小x2
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        addNewEntry(key, value, hash, index);
        return null;
    }

需要说明的是这句

int index = hash & (tab.length - 1);

首先HashMap 采用一种所谓的Hash 算法来决定每个元素的存储位置。 Java中Object提供一个hashCode()方法,也就是说每个对象都有hashCode()方法得到一个唯一的hash值
上面得到index的方法非常巧妙,它总是通过 h &(table.length -1) 来得到该对象的保存位置——而 HashMap 底层数组的长度总是 2 的 n 次方,当 length 总是 2 的倍数时,h & (length-1) 将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h & length - 1 将得到 5;如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h & length - 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。
也就是说该方法返回的值index总是在0–table.length之间循环。也就是说虽然key的hash值不同,但是他们在HashMap中的存储位置是一样的。你可能有点疑惑,我们马上就会找到答案。
那么put方法会把key-value放入HashMap,首先根据keyhash值得到在HashMap中的存储位置,如果该位置上有数据,并且key的值相等,那么直接执行修改value的操作。如果不想的会执行addNewEntry(key, value, hash, index);方法。这个方法是干什么的呢,我们来看下

void addNewEntry(K key, V value, int hash, int index) {
        table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
    }

代码很简单,生成一个新的Entry,并且把key、value、hash相应赋值,然后最重要的是把HashMapindex位置的Entry的引用付给了新Entrynext变量。也就是说在这里形成了一个Entry链,并且新的Entry位于Entry链的头部,然后把index的位置上的值更改成了新生成的Entry
HashMapget方法跟put方法的逻辑基本是一致的,感兴趣的读者可以自己查阅。
那么分析到这里我们也就理解了HashMap的基本结构,用一张图表示
这里写图片描述
可以看到HashMap中维护了一个数组,并且先用数组进行定位查找,然后用链表维护数组中同一位置上的Entry。所以说HashMap效率高不是没有道理的。

LinkedHashMap的实现原理

同样的先看下构造方法:

    public LinkedHashMap(
            int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        header = new LinkedEntry<K, V>();
        this.accessOrder = accessOrder;
    }

在LruCache中是这样使用的:
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
注意accessOrder的值为true,
首先调用父类也就是HashMap的构造方法生成一个数组,然后new了一个LinkedEntry做为header,看下LinkedEntry结构

    static class LinkedEntry<K, V> extends HashMapEntry<K, V> {
        LinkedEntry<K, V> nxt;
        LinkedEntry<K, V> prv;

        /** Create the header entry */
        LinkedEntry() {
            super(null, null, 0, null);
            nxt = prv = this;
        }
                /** Create a normal entry */
        LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
                    LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {
            super(key, value, hash, next);
            this.nxt = nxt;
            this.prv = prv;
        }
   }

可以看到它继承自HashMap中的HashMapEntry,那么其中的数据就有: key、value、hash、next、nxt、prv。
可以看到LinkedHashMapHashMap的基础上又增加了双向链表。也就是说LinkedHashMap的结构是数组+单向链表+双向链表。
那么这个双向链表是做什么用的呢。我们继续分析。
还记得HashMap中的preModify方法吗,LinkedHashMap实现了该方法用于put时替换index位置已经存在并且key值相等的节点:

    @Override void preModify(HashMapEntry<K, V> e) {
        if (accessOrder) {
            // accessOrder为true
            makeTail((LinkedEntry<K, V>) e);
        }
    }
    private void makeTail(LinkedEntry<K, V> e) {
        // Unlink e
        e.prv.nxt = e.nxt;
        e.nxt.prv = e.prv;

        // Relink e as tail
        LinkedEntry<K, V> header = this.header;
        LinkedEntry<K, V> oldTail = header.prv;
        e.nxt = header;
        e.prv = oldTail;
        oldTail.nxt = header.prv = e;
        modCount++;
    }

可以看到首先把e从双向链表中移除,然后放到链表的最尾端。可以看到其中的链表不仅是双向的而且是循环的。联系到HashMap中的put中的的操作,这一步只是修改value值并且更改在双向循环链表中的位置。
再看下LinkedHashMap中的addNewEntry方法:

    @Override void addNewEntry(K key, V value, int hash, int index) {
        LinkedEntry<K, V> header = this.header;

        // Remove eldest entry if instructed to do so.
        LinkedEntry<K, V> eldest = header.nxt;
        if (eldest != header && removeEldestEntry(eldest)) {
            remove(eldest.key);
        }

        // Create new entry, link it on to list, and put it into table
        LinkedEntry<K, V> oldTail = header.prv;
        LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
                key, value, hash, table[index], header, oldTail);
        table[index] = oldTail.nxt = header.prv = newTail;
    }

做的事情是生成一个新的LinkedEntry,把它的引用放到双向循环列表的尾部,并且把它的引用赋值到数组中相应位置上的值。
再看下get方法

    @Override public V get(Object key) {
        /*
         * This method is overridden to eliminate the need for a polymorphic
         * invocation in superclass at the expense of code duplication.
         */
        if (key == null) {
            HashMapEntry<K, V> e = entryForNullKey;
            if (e == null)
                return null;
            if (accessOrder)
                makeTail((LinkedEntry<K, V>) e);
            return e.value;
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                if (accessOrder)
                    makeTail((LinkedEntry<K, V>) e);
                return e.value;
            }
        }
        return null;
    }

逻辑跟HashMap基本一样,根据index在数组中定位,然后在单向列表中查找。也就是不管我们在LinkedHashMap中执行get方法或者put方法,最后操作的那个entry都是插到双向链表的尾部。那么头部的nxt的那个entry也就是最久未使用的entry依据这个特性我们就可以实现LRU实现缓存。
依据看一张图
这里写图片描述
然而分析道这里实际上并没有用到双向循环列表,那么这个双向循环列表到底是干什么用的呢?
我们看下LruCache中的put方法

    public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            putCount++;
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }

        trimToSize(maxSize);
        return previous;
    }
    public void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }

                if (size <= maxSize || map.isEmpty()) {
                    break;
                }

                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

最后如果当前大小大于我们设定的最大缓存大小那么执行trimToSize方法中根据LinkedHashMapentrySet得到iterator,然后执行next方法,最后执行的是LinkedHashMap中的这些代码

 LinkedEntry<K, V> next = header.nxt;
        LinkedEntry<K, V> lastReturned = null;
        int expectedModCount = modCount;

        public final boolean hasNext() {
            return next != header;
        }

        final LinkedEntry<K, V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            LinkedEntry<K, V> e = next;
            if (e == header)
                throw new NoSuchElementException();
            next = e.nxt;
            return lastReturned = e;
        }

可以看到返回的是header.nxt,也就是双向循环链表中的第一个entry,那么也就是最久未使用的那个entry

总结

到此LruCache的基本原理我们都分析清楚了,并且梳理了HashMap和LinkedHashMap的内部实现原理,虽然我们在开发项目的时候对基本数据结构的内部原理不太关注,但是我们也应该学习其内部原理。毕竟基础才是最重要的。说到HashMap又想到了androidSDK中的SparseArray可以替代key为Integer类型的HashMap,感性却的可以看这篇文章

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Android应用性能优化之使用SparseArray替代HashMap

Android SparseArray代替HashMap提升性能

LruCache 实现原理分析

LruCache 实现原理分析最近研究了一下LruCahce的实现原理,以前也看过几遍源码了,但是有些还是没有理解清楚。重新撸了一遍代码,吼吼吼。声明的变量 private final Link...

Dagger2 这次入门就不用放弃了

Dagger2 这次入门就不用放弃了前言之前也研究过很多次Dagger2这东西了,可能以后RxJava+Retrofit+MVP+Dagger2是Android发展的主流框架,看了Dagger2的实现...

LruCache 实现原理分析

LruCache 实现原理分析最近研究了一下LruCahce的实现原理,以前也看过几遍源码了,但是有些还是没有理解清楚。重新撸了一遍代码,吼吼吼。声明的变量 private final Link...

Android LruCache 实现原理解析

介绍

LRUCache的实现原理

简介LRU(Least Recently Used)最近最少使用,最近有时间和空间最近的歧义,所以我更喜欢叫它近期最少使用算法。它的核心思想是,如果一个数据被访问过,我们有理由相信它在将来被访问的概率...

Android提供的LruCache类简介

package android.util;      import java.util.LinkedHashMap;   import java.util.Map;     ...

STL系列之九 探索hash_set

Title:        STL系列之九 探索hash_setAuthor:     MoreWindowsBlog:       htt...

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

前言:有网友建议分析下LinkedHashMap的源码,于是花了一晚上时间研究了下,分享出此文,希望大家相互学习。LinkedHashMap的源码理解起来也不难(当然,要建立在你对HashMap源码有...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)