Android面試題解析第一天 (• LRUCache 原理)

面試是一場與面試官交心的過程,會遇到一些成熟穩重的大牛、同樣也會遇到一些設計挖苦你自以爲是的人,這些都不重要,我們能夠做到的只有好好掌握知識,一點點的積累。

LRU( Least Recently Used ) 算法,經常會在面試中問到,雖然名字聽起來高大上,但是算法其實很簡單,最近最少使用的就將其排除在列表之外,以便將最近最常使用的節點放在列表最前面,在取數據的時候方便快捷的拿到數據,提高性能,其實說到這裏,聰明的你應該都知道了其中的原理,下面我們通過其中的源碼進行分析。


面試題:LRU算法實現原理以及在項目中的應用

上面說到,最近最少使用將排除在列表之外,那這個列表是什麼?? 通過源碼得知,jdk中實現的LRU算法內部持有了一個LinkedHashMap:

/**
 *一個基於LinkedHashMap的LRU緩存淘汰算法,
 * 這個緩存淘汰列表包含了一個最大的節點值,如果在列表滿了之後再有額外的值添加進來,
 * 則LRU(最近最少使用)的節點將被移除列表外
 * 
 * 當前類是線程安全的,所有的方法都被同步了
 */
public class LRUCache<K, V> {

    private static final float hashTableLoadFactor = 0.75f;
    private LinkedHashMap<K, V> map;
    private int cacheSize;

    //...
}

而LinkedHashMap內部節點的特性就是一個雙向鏈表,有頭結點和尾節點,有下一個指針節點和上一個指針節點;LRUCache中,主要是put() 與 get () 兩個方法,LinkedHashMap是繼承至HashMap,查看源碼得知LinkedHashMap並沒有重寫父類的put方法,而是實現了其中另外一個方法,afterNodeInsertion() 接下來分析一下LinkedHashMap中的put() 方法:

    // 由LinkedHashMap 實現回調
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }
// HashMap.put()
 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        // 回調給LinkedHashMap ,evict爲boolean
        afterNodeInsertion(evict);
        return null;
    }

可以看到新增節點的方法,是由父類實現,並傳遞迴調函數afterNodeInsertion(evict) 給LinkedHashMap實現:

 void afterNodeInsertion(boolean evict) { // 可能移除最老的節點
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

可以看到 if 中有一個removeEldestEntry(first) 方法,該方法是給用戶去實現的,該怎樣去移除這個節點,最常用的是判斷當前列表的長度是否大於緩存節點的長度:

 this.map = new LinkedHashMap<K, V>(hashTableCapacity, LRUCache.hashTableLoadFactor, true) {
            // (an anonymous inner class)

            private static final long serialVersionUID = 1;

            @Override
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                // 返回爲true的話可能移除節點
                return this.size() > LRUCache.this.cacheSize;
            }
        };

接下來就是get() 方法了:

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            // 調用HashMap給LinkedHashMap的回調方法
            afterNodeAccess(e);
        return e.value;
    }
// afterNodeAccess(e)
 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;
        }
    }

所以在這個方法中,就將我們上面說到的最近最常使用的節點移到最後,而最近最少使用的節點自然就排列到了前面,如果需要移除的話,就從前面刪除掉了節點。

總結

說了這麼多,又是LRUCache使用,又是LinkedHashMap使用,面試的時候,我跟面試官說這麼多源碼有啥用啊? 其實看完源碼,我們對原理應該瞭解得很清晰了,LRU算法就是淘汰算法,其中內置了一個LinkedHashMap來存儲數據。

比如我們的圖片加載框架Glide,其中大部分算法都是LRU算法;有內存緩存算法和磁盤緩存算法(DiskLRUCache); 當緩存的內存達到一定限度時,就會從列表中移除圖片(當然Glide有活動內存和內存兩個,並不是直接刪除掉)。

面試的時候,說到LinkedHashMap中的幾個HashMap回調方法與其中操作,再聯繫一些常用的框架及具體實現,相信會事倍功半吧^~^

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章