LinkedHashMap原理(淺談)

一. 回顧

前面深入瞭解了HashMap實現原理以及HashMap常見問題,今天簡略瞭解LinkedHashMap原理。

此筆記僅供自己學習完後複習回顧參考,還有很多待提高的地方,如有錯誤請指正

二. LinkedHashMap

打開IDEA,按兩下shift搜索LinkedHashMap,打開源碼,可以看到如下:

 * @since   1.4
 */
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{

解釋: @since1.4代表LinkedHashMap在jdk1.4纔開始有。LinkedHashMap繼承了HashMap,實現了Map接口。由此可以看出LinkedHashMap是一個HashMap,推理出其在HashMap的基礎上增加了某些東西。

2.1 成員變量

head、tail: 從中文意思可看出這是節點的頭尾指針,也就是在HashMap的基礎上加了頭尾指針。 指針的類型是LinkedHashMap.Entry<K,V>,它是LinkedHashMap的靜態內部類,後面再詳述。

 /**
     * 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;

accessOrder: 從中文意思可看出這是訪問順序。這也是在HashMap的基礎上加的東西。註釋中作出瞭解釋,這是給LinkedHashMap作迭代用的。當值爲true的時候,該變量代表訪問順序(後面再用例子詳述什麼是訪問順序);當值爲false的時候,該變量代表插入順序。(即存鍵值對進去是什麼順序,迭代打印出來的就是什麼順序)。

/**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;

2.2 構造方法

因爲LinkedHashMap繼承了HashMap,所以兩者底層架構並沒有很大的差異(同樣都是數組+鏈表+紅黑樹)。

兩者的差別就在:LinkedHashMap的數據節點多了2個指針:頭指針head尾指針tail

它有5個構造方法,比HashMap多了1個,其他的四個構造方法和HashMap的都差不多。如下:
在這裏插入圖片描述


LinkedHashMap(): 無參構造,調用父類HashMap的無參構造(初始容量爲16,負載因子爲0.75),並且設置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;
    }

LinkedHashMap(int initialCapacity): 調用父類HashMap(initialCapacity),指定初始容量,並且設置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;
    }

LinkedHashMap(int initialCapacity, float loadFactor): 調用父類HashMap(initialCapacity, loadFactor),指定初始容量和負載因子,並且設置accessOrder=false;(即使用插入順序)

 /**
     * 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;
    }

LinkedHashMap(Map<? extends K, ? extends V> m): 調用父類HashMap的空參構造,並且設置accessOrder=false;(即使用插入順序),調用putMapEntries()方法將map集合轉爲LinkedHashMap集合。容量由入參map集合決定

/**
     * 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);
    }

LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder):
調用父類HashMap的(initialCapacity, loadFactor)並且設置指定的accessOrder(即使用指定的順序)此三個入參的構造方法在HashMap並沒有

/**
     * 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;
    }

2.3 靜態內部類

這是增加了head、tail兩個指針的類型,繼承了HashMap.Node<K,V>,也就是說擁有HashMap.Node<K,V>的特性。構造方法調用了HashMap.Node<K,V>的Node(int hash, K key, V value, Node<K,V> next)

 /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    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);
        }
    }

2.4 linkNodeLast()方法

在鏈表末尾添加節點,如下:

// link at the end of list
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }

可以看出LinkedHashMap加了head、tail的指針,作用和C語言的鏈表差不多。

2.5 訪問順序

上面介紹成員變量accessOrder的時候,涉及到訪問順序這個詞。


在解釋這個知識點之前,先回顧一下LRU知識點

LRU:Least Recently Used,中文是最近最少使用。是一種常用的頁面置換算法。在內存不命中的條件下,內存缺頁需要將頁面置換。詳情如下:
在這裏插入圖片描述


下面用一個測試實例來解釋說明。
下面的測試目的是證明:

  1. HashMap插入的順序與取出的順序是不一樣的,即HashMap是無序的
  2. LinkedHashMap插入的順序與取出的順序是一樣的,即LinkedHashMap是有序的
  @Test
    public void test1(){
        Map<String, String> linkMap = new LinkedHashMap<String, String>();
        Map<String, String> hashMap = new HashMap<String, String>();

        linkMap.put("2", "2");
        linkMap.put("1", "1");
        linkMap.put("3", "3");

        hashMap.put("2", "2");
        hashMap.put("1", "1");
        hashMap.put("3", "3");

        Set<Map.Entry<String, String>> linkEntries = linkMap.entrySet();
        Set<Map.Entry<String, String>> hashEntries = hashMap.entrySet();

        System.out.print("LinkedHashMap:");
        for (Map.Entry<String, String> entry : linkEntries) {
            System.out.print(" " + entry + " ");
        }

        System.out.println();

        System.out.print("HashMap:");
        for (Map.Entry<String, String> entry : hashEntries) {
            System.out.print(" " + entry + " ");
        }
    }

測試結果:
在這裏插入圖片描述


下面測試開啓accessOrder訪問順序(即accessOrder=true)。如下:

@Test
    public void test2() {
        //使用三個入參的構造器來設置accessOrder=true
        Map<String, String> linkMap =
                new LinkedHashMap<String, String>(16, 0.75f, true);

        linkMap.put("2", "2");
        linkMap.put("1", "1");
        linkMap.put("3", "3");

        Set<Map.Entry<String, String>> linkEntries = linkMap.entrySet();//獲取鍵值對集合

        System.out.print("訪問前的LinkedHashMap:");
        for (Map.Entry<String, String> entry : linkEntries) {
            System.out.print(" " + entry + " ");
        }
        System.out.println();

        //訪問linkMap
        linkMap.get("2");
        System.out.print("訪問2後的LinkedHashMap:");
        for (Map.Entry<String, String> entry : linkEntries) {
            System.out.print(" " + entry + " ");
        }
        System.out.println();
        
        //訪問linkMap
        linkMap.get("1");
        System.out.print("訪問1後的LinkedHashMap:");
        for (Map.Entry<String, String> entry : linkEntries) {
            System.out.print(" " + entry + " ");
        }


    }

測試結果:
在這裏插入圖片描述
可以看到被訪問後的元素,被置頂了。也就是說,LinedHashMap訪問某元素,某元素將被置頂。這樣的功能應用在LRU緩存方面。 經常訪問的元素就可以很快速地找到了,而不被經常訪問的元素可以沉到底甚至可以被覆蓋,LinkedHahsMap這樣的功能使得它在緩存技術方面發揚光大。

總結:從上面的訪問順序,也可以總結出,遍歷LinkedHashMap是按照從鏈表開始掃描,掃到尾部的。get元素後,元素被放到鏈表的尾部,所以每次get後再遍歷LinkedHashMap,get的元素都會出現在末尾。

2.6 LinkedHashMap是如何實現LRU算法?

Mybatis中的Lrucache底層是用LinkedHashMap。我們從源碼分析LinkedHashMap怎麼可以實現LRU。

因爲涉及的是訪問順序,而它又是因get()方法引起的,所以首先從get()方法看起,如下:

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            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;
        }
    }

看到方法開頭就已經有一行註釋了// move node to last,意思就是將元素移到鏈表尾部。

總結:當訪問某元素,如果設置了訪問順序accessOrder=true,則該元素被移到鏈表尾部。

按照這個思路,如果滿足當前長度大於某值就移除鏈頭的元素,這樣就實現了LRU緩存。

由此可推理當我們put元素進去,LinkedHashMap會判斷元素是否已經達到了LRU設定的緩存大小,如果達到就會實行LRU。將很久不被使用的元素移除。因此我們查看LinkedHashMap的put()方法,發現沒有這個方法。那說明LinkedHashMap的put方法調用的是父類HashMap的put()方法。打開HashMap找到put()方法,發現有一個afterNodeInsertion(),如下:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
上圖中此afterNodeInsertion()方法並沒有在HashMap中實現而是在LinkedHashMap實現。

打開LinkedHashMap查看afterNodeInsertion()方法,如下:
在這裏插入圖片描述
點進去看removeEldestEntry(first)該方法默認是返回false(即默認不實現緩存功能)。如下:

 protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

總結:綜上所述只要我們重寫這個方法在某條件下返回true就可以實現LRU緩存。

測試例子:用LinkedHashMap實現LRU緩存

 @Test
    public void test3() {
        Map<String, String> map =
                new LinkedHashMap<String, String>(5, 0.75f, true) {
                    @Override
                    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                        //當前長度>=5,再插入就移除。即達到閾值時就實現LRU
                        return this.size()>=5;
                    }
                };

        map.put("2", "2");
        map.put("1", "1");
        map.put("3", "3");
        map.put("9", "9");

        Set<Map.Entry<String, String>> linkEntries = map.entrySet();//獲取鍵值對集合

        //訪問linkMap
        map.get("2");
        System.out.print("訪問2後的LinkedHashMap:");
        for (Map.Entry<String, String> entry : linkEntries) {
            System.out.print(" " + entry + " ");
        }
        System.out.println();

        map.put("5", "5");

        System.out.print("存入5後的LinkedHashMap:");
        for (Map.Entry<String, String> entry : linkEntries) {
            System.out.print(" " + entry + " ");
        }
        System.out.println();
    }

測試結果:
在這裏插入圖片描述

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