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

来源:互联网 发布:学软件软件好的学校 编辑:程序博客网 时间:2024/06/06 09:56

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,感性却的可以看这篇文章

1 0
原创粉丝点击