LRU與LinkedHashMap的不解情緣

1.前言

最近在學習Mybatis源碼關於一級和二級緩存的過程中,有這麼一個類,LruCache.class。按照設計模式來說這裏用到了裝飾者的設計模式,維護一個接口類【Cache delegate;】的變量。當然這不是重點,重點是他有一個成員變量,keyMap,實例化對象是LinkedHashMap。更要緊的是,他在創建這個對象的時候,還重寫了removeEldestEntry這個方法(如下代碼),那麼LRU是個什麼鬼,這個算法與LinkedHashMap又有什麼關係,讓我們走進LinkedHashMap的源碼,看看究竟是一個說明鬼?

//org.apache.ibatis.cache.decorators.LruCache
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;
      }
    };
  }

2. LRU概述

在進入LinkedHashMap源碼之前,我們有必要先了解一下什麼是LRU。LRU是Least Recently Used的縮寫,即最近最少使用,是一種常用的頁面置換算法,選擇最近最久未使用的頁面予以淘汰。該算法賦予每個頁面一個訪問字段,用來記錄一個頁面自上次被訪問以來所經歷的時間 t,當須淘汰一個頁面時,選擇現有頁面中其 t 值最大的,即最近最少使用的頁面予以淘汰。(允許我懶一把,摘自百度百科).。用一句話概括,爲了減少性能消耗,LRU會將最舊的元素移除,從而保證緩存總個數恆定。那麼就需要解決兩個問題:1)如何判斷哪個元素最舊;2)何時進行移除最舊元素。帶着這些問題,我們就踏進LinkedHashMap的世界吧。

3 LinkedHashMap

3.1 概述

根據JDK官方提供的說明文檔,我們知道這麼幾件事:

1)它維護了兩個變量,head和tail,採用雙向鏈表的形式展現;

2)他提供了一個特殊的構造函數{@link #LinkedHashMap(int,float,boolean)},其迭代順序是其條目的最近訪問順序(訪問順序),這種映射非常適合構建LRU緩存。可以重寫{@link #removeEldestEntry(Map.Entry)}方法,以強加一個策略,以便在將新映射添加到map集合時自動刪除陳舊的映射。

好了,這兩點是我們最爲關注的。

3.2 HashMap爲LinkedHashMap做了哪些準備

翻開HashMap的源碼,我們會發現有三個特殊的方法。

// Callbacks to allow LinkedHashMap post-actions
//鉤子方法
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

JDK給出的解釋是允許LinkedHashMap後置回調。那麼這幾個方法都在什麼地方出現了,我們通篇查找了一下,在HashMap中,被調用的方法包括:

1.afterNodeAccess
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
public boolean replace(K key, V oldValue, V newValue)
public V replace(K key, V value)
public V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction)
public V computeIfPresent(K key,BiFunction<? super K,? super V,? extends V> remapping)
public V get(Object key)[from LinkedHashMap]
public V getOrDefault(Object key, V defaultValue)[from LinkedHashMap]

2.afterNodeInsertion
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
public V compute(K key,BiFunction<? super K, ? super V, ? extends V> remapping)
public V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remapping)

3.afterNodeRemoval
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable)

這裏基本囊括了HashMap的增刪改查所有的方法,不過需要指出的是HashMap的get相關的方法並沒有調用afterNodeAccess,而是交給了LinkedHashMap調用。那麼這三個方法都做了什麼事?讓我逐個看一下。

3.3 afterNodeAccess

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;
            //將當前訪問節點追加到最後一位
            //如果last爲空,則p還是頭部節點
            if (last == null)
                head = p;
            else {
                //雙向鏈表追加到尾部
                p.before = last;
                last.after = p;
            }
            //此時更新尾部節點爲p
            tail = p;
            ++modCount;
        }
    }

這裏有一句關鍵的代碼:if (accessOrder && (last = tail) != e) 。對於accessOrder,jdk給出的解釋是:

The iteration ordering method for this linked hash map: true for access-order,false for insertion-order.
用於linkedHashMap的迭代排序方法:true->訪問排序;false:插入排序

而(last = tail) != e判斷是否是尾部元素,如果是那就不用那麼麻煩了,否則將此元素放到鏈表的尾部,同時維護鏈表的雙向性。具體代碼大家可以自行分析一下。寫到這麼,大家明白LinkedHashMap爲什麼要用雙向鏈表來存儲數據了吧。

3.4 afterNodeRemoval

void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    //前置節點爲null,則之前e爲頭結點,現在將e移除,則e的後置節點b爲頭結點
    if (b == null)
        head = a;
    else
        //將e的前置節點的後置節點設置爲e的後置節點
        b.after = a;
        //雙向鏈表設置同理
        if (a == null)
            tail = b;
        else
            a.before = b;
}

這段代碼很好理解,就是將當前節點移除,同時維護鏈表的雙向性。

3.5 afterNodeInsertion

這個方法是這三個方法中最重要的一個方法。前面提到他可以作爲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);
    }
}

咦,雖然這個方法對於LRU起到關鍵性的作用,但是彷彿並沒有那麼複雜。無非就是進行一系列判斷,然後決定是否移除元素。這個最舊的元素是怎麼確定的?通過上面的兩個方法我們知道HashMap經過增刪改查後,都會將當前元素放到鏈表的最後。那麼我們就可以認爲第一個元素就是最舊的元素。找到最舊的元素後,那麼就是如何判斷最舊的元素是否應該被刪除。,通過代碼我們很容易知道這項任務交給了判斷語句上,if (evict && (first = head) != null && removeEldestEntry(first));這裏有三個條件判斷,只有這三個條件全部滿足,纔可以刪除最舊的元素。

1)evict。除了putVal之外,其餘傳遞的都是true。putVal中也即是在clone方法和以一個就的Map初始化新的HashMap的時候evict才爲false,也即是這兩個操作不會移除Map中的元素。

2)(first = head) != null。意思是頭部元素不爲空的情況下爲true。也就是說,如果頭部元素爲空,意味着這個鏈表爲空鏈表,也就不需要移除元素。

3)removeEldestEntry(first)。這個方法纔是這個類的靈魂所在。默認實現是返回false,也就是默認不會刪除最舊元素。當要構建一個LRU算法的過程中,必須重寫這個方法,如果當前元素個數超過指定的個數,這樣就返回true。一旦返回true,那最舊的元素就會被刪除。

4. 總結

分析到這裏,我們終於明白LinkedHashMap是怎麼實現LRU的,他的一個關鍵方法便是removeEldestEntry()。回到文章開始,我們再來看一下那段代碼。

//org.apache.ibatis.cache.decorators.LruCache
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中的元素個數大於指定的size的時候,就要返回true。對於eldestKey = eldest.getKey();那是要從PerpetualCache中移除最舊元素。對於PerpetualCache,有興趣的可以自行看一下。

5 題外話

Mybatis中真正的二級緩存其實是PerpetualCache,而LRUCache只不過是他的一種回收策略。

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