Java集合詳解5:深刻理解LinkedHashMap和LRU緩存

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到個人倉庫裏查看html

https://github.com/h2pl/Java-Tutorial前端

喜歡的話麻煩點下Star哈java

文章首發於個人我的博客:node

www.how2playlife.compython

本文是微信公衆號【Java技術江湖】的《走進JavaWeb技術世界》其中一篇,本文部份內容來源於網絡,爲了把本文主題講得清晰透徹,也整合了不少我認爲不錯的技術博客內容,引用其中了一些比較好的博客文章,若有侵權,請聯繫做者。git

該系列博文會告訴你如何從入門到進階,從servlet到框架,從ssm再到SpringBoot,一步步地學習JavaWeb基礎知識,並上手進行實戰,接着瞭解JavaWeb項目中常常要使用的技術和組件,包括日誌組件、Maven、Junit,等等內容,以便讓你更完整地瞭解整個JavaWeb技術體系,造成本身的知識框架。爲了更好地總結和檢驗你的學習成果,本系列文章也會提供每一個知識點對應的面試題以及參考答案。程序員

若是對本系列文章有什麼建議,或者是有什麼疑問的話,也能夠關注公衆號【Java技術江湖】聯繫做者,歡迎你參與本系列博文的創做和修訂。github

文末贈送8000G的Java架構師學習資料,須要的朋友能夠到文末了解領取方式,資料包括Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源)
面試

今天咱們來深刻探索一下LinkedHashMap的底層原理,而且使用linkedhashmap來實現LRU緩存。算法

摘要:

HashMap和雙向鏈表合二爲一便是LinkedHashMap。所謂LinkedHashMap,其落腳點在HashMap,所以更準確地說,它是一個將全部Entry節點鏈入一個雙向鏈表的HashMap。

因爲LinkedHashMap是HashMap的子類,因此LinkedHashMap天然會擁有HashMap的全部特性。好比,LinkedHashMap的元素存取過程基本與HashMap基本相似,只是在細節實現上稍有不一樣。固然,這是由LinkedHashMap自己的特性所決定的,由於它額外維護了一個雙向鏈表用於保持迭代順序。

此外,LinkedHashMap能夠很好的支持LRU算法,筆者在第七節便在LinkedHashMap的基礎上實現了一個可以很好支持LRU的結構。

友情提示:

  本文全部關於 LinkedHashMap 的源碼都是基於 JDK 1.6 的,不一樣 JDK 版本之間也許會有些許差別,但不影響咱們對 LinkedHashMap 的數據結構、原理等總體的把握和了解。後面會講解1.8對於LinkedHashMap的改動。

  因爲 LinkedHashMap 是 HashMap 的子類,因此其具備HashMap的全部特性,這一點在源碼共用上體現的尤其突出。所以,讀者在閱讀本文以前,最好對 HashMap 有一個較爲深刻的瞭解和回顧,不然極可能會致使事倍功半。能夠參考我以前關於hashmap的文章。

LinkedHashMap 概述

  筆者曾提到,HashMap 是 Java Collection Framework 的重要成員,也是Map族(以下圖所示)中咱們最爲經常使用的一種。不過遺憾的是,HashMap是無序的,也就是說,迭代HashMap所獲得的元素順序並非它們最初放置到HashMap的順序。

  HashMap的這一缺點每每會形成諸多不便,由於在有些場景中,咱們確須要用到一個能夠保持插入順序的Map。慶幸的是,JDK爲咱們解決了這個問題,它爲HashMap提供了一個子類 —— LinkedHashMap。雖然LinkedHashMap增長了時間和空間上的開銷,可是它經過維護一個額外的雙向鏈表保證了迭代順序。
  

  特別地,該迭代順序能夠是插入順序,也能夠是訪問順序。所以,根據鏈表中元素的順序能夠將LinkedHashMap分爲:保持插入順序的LinkedHashMap和保持訪問順序的LinkedHashMap,其中LinkedHashMap的默認實現是按插入順序排序的。

     image      

  本質上,HashMap和雙向鏈表合二爲一便是LinkedHashMap。所謂LinkedHashMap,其落腳點在HashMap,所以更準確地說,它是一個將全部Entry節點鏈入一個雙向鏈表雙向鏈表的HashMap。

  在LinkedHashMapMap中,全部put進來的Entry都保存在以下面第一個圖所示的哈希表中,但因爲它又額外定義了一個以head爲頭結點的雙向鏈表(以下面第二個圖所示),所以對於每次put進來Entry,除了將其保存到哈希表中對應的位置上以外,還會將其插入到雙向鏈表的尾部。

image

  更直觀地,下圖很好地還原了LinkedHashMap的原貌:HashMap和雙向鏈表的密切配合和分工合做造就了LinkedHashMap。特別須要注意的是,next用於維護HashMap各個桶中的Entry鏈,before、after用於維護LinkedHashMap的雙向鏈表,雖然它們的做用對象都是Entry,可是各自分離,是兩碼事兒。

這裏寫圖片描述
  
  其中,HashMap與LinkedHashMap的Entry結構示意圖以下圖所示:

這裏寫圖片描述

  特別地,因爲LinkedHashMap是HashMap的子類,因此LinkedHashMap天然會擁有HashMap的全部特性。好比,==LinkedHashMap也最多隻容許一條Entry的鍵爲Null(多條會覆蓋),但容許多條Entry的值爲Null。==
  
  此外,LinkedHashMap 也是 Map 的一個非同步的實現。此外,LinkedHashMap還能夠用來實現LRU (Least recently used, 最近最少使用)算法,這個問題會在下文的特別談到。

LinkedHashMap 在 JDK 中的定義

類結構定義

  LinkedHashMap繼承於HashMap,其在JDK中的定義爲:

public class LinkedHashMap<K,V> extends HashMap<K,V>
    implements Map<K,V> {

    ...
}

成員變量定義

  與HashMap相比,LinkedHashMap增長了兩個屬性用於保證迭代順序,分別是 雙向鏈表頭結點header 和 標誌位accessOrder (值爲true時,表示按照訪問順序迭代;值爲false時,表示按照插入順序迭代)。

/**
 * The head of the doubly linked list.
 */
private transient Entry<K,V> header;  // 雙向鏈表的表頭元素

/**
 * The iteration ordering method for this linked hash map: <tt>true</tt>
 * for access-order, <tt>false</tt> for insertion-order.
 *
 * @serial
 */
private final boolean accessOrder;  //true表示按照訪問順序迭代,false時表示按照插入順序

成員方法定義

  從下圖咱們能夠看出,LinkedHashMap中並增長沒有額外方法。也就是說,LinkedHashMap與HashMap在操做上大體相同,只是在實現細節上略有不一樣罷了。

[外鏈圖片轉存失敗(img-C2vYmjQ7-1567839753833)(http://static.zybuluo.com/Rico123/nvojgv4s0o0ciieibz1tbakc/LinkedHashMap_Outline.png)]

基本元素 Entry

  LinkedHashMap採用的hash算法和HashMap相同,可是它從新定義了Entry。LinkedHashMap中的Entry增長了兩個指針 before 和 after,它們分別用於維護雙向連接列表。特別須要注意的是,next用於維護HashMap各個桶中Entry的鏈接順序,before、after用於維護Entry插入的前後順序的,源代碼以下:

private static class Entry<K,V> extends HashMap.Entry<K,V> {

    // These fields comprise the doubly linked list used for iteration.
    Entry<K,V> before, after;

    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
        super(hash, key, value, next);
    }
    ...
}

  形象地,HashMap與LinkedHashMap的Entry結構示意圖以下圖所示:

這裏寫圖片描述

LinkedHashMap 的構造函數

  LinkedHashMap 一共提供了五個構造函數,它們都是在HashMap的構造函數的基礎上實現的,除了默認空參數構造方法,下面這個構造函數包含了大部分其餘構造方法使用的參數,就不一一列舉了。

LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)

  該構造函數意在構造一個指定初始容量和指定負載因子的具備指定迭代順序的LinkedHashMap,其源碼以下:

/**
* Constructs an empty LinkedHashMap 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 - true for
* access-order, false 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); // 調用HashMap對應的構造函數
this.accessOrder = accessOrder; // 迭代順序的默認值
}

初始容量 和負載因子是影響HashMap性能的兩個重要參數。一樣地,它們也是影響LinkedHashMap性能的兩個重要參數。此外,LinkedHashMap 增長了雙向鏈表頭結點 header和標誌位 accessOrder兩個屬性用於保證迭代順序。
  

LinkedHashMap(Map<? extends K, ? extends V> m)

  該構造函數意在構造一個與指定 Map 具備相同映射的 LinkedHashMap,其 初始容量不小於 16 (具體依賴於指定Map的大小),負載因子是 0.75,是 Java Collection Framework 規範推薦提供的,其源碼以下:

/**
 * 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(m);       // 調用HashMap對應的構造函數
    accessOrder = false;    // 迭代順序的默認值
}

init 方法

  從上面的五種構造函數咱們能夠看出,不管採用何種方式建立LinkedHashMap,其都會調用HashMap相應的構造函數。事實上,無論調用HashMap的哪一個構造函數,HashMap的構造函數都會在最後調用一個init()方法進行初始化,只不過這個方法在HashMap中是一個空實現,而在LinkedHashMap中重寫了它用於初始化它所維護的雙向鏈表。例如,HashMap的參數爲空的構造函數以及init方法的源碼以下:

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();
}

/**
* Initialization hook for subclasses. This method is called
* in all constructors and pseudo-constructors (clone, readObject)
* after HashMap has been initialized but before any entries have
* been inserted. (In the absence of this method, readObject would
* require explicit knowledge of subclasses.)
*/
void init() {
}

  在LinkedHashMap中,它重寫了init方法以便初始化雙向列表,源碼以下:

/**
* Called by superclass constructors and pseudoconstructors (clone,
* readObject) before any entries are inserted into the map. Initializes
* the chain.
*/
void init() {
header = new Entry<K,V>(-1, null, null, null);
header.before = header.after = header;
}

  所以,咱們在建立LinkedHashMap的同時就會不知不覺地對雙向鏈表進行初始化。

LinkedHashMap 的數據結構

本質上,LinkedHashMap = HashMap + 雙向鏈表,也就是說,HashMap和雙向鏈表合二爲一便是LinkedHashMap。

也能夠這樣理解,LinkedHashMap 在不對HashMap作任何改變的基礎上,給HashMap的任意兩個節點間加了兩條連線(before指針和after指針),使這些節點造成一個雙向鏈表。

在LinkedHashMapMap中,全部put進來的Entry都保存在HashMap中,但因爲它又額外定義了一個以head爲頭結點的空的雙向鏈表,所以對於每次put進來Entry還會將其插入到雙向鏈表的尾部。

            這裏寫圖片描述

LinkedHashMap 的快速存取

  咱們知道,在HashMap中最經常使用的兩個操做就是:put(Key,Value) 和 get(Key)。一樣地,在 LinkedHashMap 中最經常使用的也是這兩個操做。

對於put(Key,Value)方法而言,LinkedHashMap徹底繼承了HashMap的 put(Key,Value) 方法,只是對put(Key,Value)方法所調用的recordAccess方法和addEntry方法進行了重寫;對於get(Key)方法而言,LinkedHashMap則直接對它進行了重寫。

下面咱們結合JDK源碼看 LinkedHashMap 的存取實現。

LinkedHashMap 的存儲實現 : put(key, vlaue)

  上面談到,LinkedHashMap沒有對 put(key,vlaue) 方法進行任何直接的修改,徹底繼承了HashMap的 put(Key,Value) 方法,其源碼以下:

public V put(K key, V value) {

    //當key爲null時,調用putForNullKey方法,並將該鍵值對保存到table的第一個位置 
    if (key == null)
        return putForNullKey(value); 

    //根據key的hashCode計算hash值
    int hash = hash(key.hashCode());           

    //計算該鍵值對在數組中的存儲位置(哪一個桶)
    int i = indexFor(hash, table.length);              

    //在table的第i個桶上進行迭代,尋找 key 保存的位置
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {      
        Object k;
        //判斷該條鏈上是否存在hash值相同且key值相等的映射,若存在,則直接覆蓋 value,並返回舊value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this); // LinkedHashMap重寫了Entry中的recordAccess方法--- (1)    
            return oldValue;    // 返回舊值
        }
    }

    modCount++; //修改次數增長1,快速失敗機制

    //原Map中無該映射,將該添加至該鏈的鏈頭
    addEntry(hash, key, value, i);  // LinkedHashMap重寫了HashMap中的createEntry方法 ---- (2)    
    return null;
}

  上述源碼反映了LinkedHashMap與HashMap保存數據的過程。特別地,在LinkedHashMap中,它對addEntry方法和Entry的recordAccess方法進行了重寫。下面咱們對比地看一下LinkedHashMap 和HashMap的addEntry方法的具體實現:

/**
 * This override alters behavior of superclass put method. It causes newly
 * allocated entry to get inserted at the end of the linked list and
 * removes the eldest entry if appropriate.
 *
 * LinkedHashMap中的addEntry方法
 */
void addEntry(int hash, K key, V value, int bucketIndex) {   

    //建立新的Entry,並插入到LinkedHashMap中  
    createEntry(hash, key, value, bucketIndex);  // 重寫了HashMap中的createEntry方法

    //雙向鏈表的第一個有效節點(header後的那個節點)爲最近最少使用的節點,這是用來支持LRU算法的
    Entry<K,V> eldest = header.after;  
    //若是有必要,則刪除掉該近期最少使用的節點,  
    //這要看對removeEldestEntry的覆寫,因爲默認爲false,所以默認是不作任何處理的。  
    if (removeEldestEntry(eldest)) {  
        removeEntryForKey(eldest.key);  
    } else {  
        //擴容到原來的2倍  
        if (size >= threshold)  
            resize(2 * table.length);  
    }  
} 

-------------------------------我是分割線------------------------------------

 /**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 * 
 * HashMap中的addEntry方法
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    //獲取bucketIndex處的Entry
    Entry<K,V> e = table[bucketIndex];

    //將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry 
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);

    //若HashMap中元素的個數超過極限了,則容量擴大兩倍
    if (size++ >= threshold)
        resize(2 * table.length);
}

  因爲LinkedHashMap自己維護了插入的前後順序,所以其能夠用來作緩存,14~19行的操做就是用來支持LRU算法的,這裏暫時不用去關心它。此外,在LinkedHashMap的addEntry方法中,它重寫了HashMap中的createEntry方法,咱們接着看一下createEntry方法:

void createEntry(int hash, K key, V value, int bucketIndex) { 
    // 向哈希表中插入Entry,這點與HashMap中相同 
    //建立新的Entry並將其鏈入到數組對應桶的鏈表的頭結點處, 
    HashMap.Entry<K,V> old = table[bucketIndex];  
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  
    table[bucketIndex] = e;     

    //在每次向哈希表插入Entry的同時,都會將其插入到雙向鏈表的尾部,  
    //這樣就按照Entry插入LinkedHashMap的前後順序來迭代元素(LinkedHashMap根據雙向鏈表重寫了迭代器)
    //同時,新put進來的Entry是最近訪問的Entry,把其放在鏈表末尾 ,也符合LRU算法的實現  
    e.addBefore(header);  
    size++;  
}

  由以上源碼咱們能夠知道,在LinkedHashMap中向哈希表中插入新Entry的同時,還會經過Entry的addBefore方法將其鏈入到雙向鏈表中。其中,addBefore方法本質上是一個雙向鏈表的插入操做,其源碼以下:

//在雙向鏈表中,將當前的Entry插入到existingEntry(header)的前面  
private void addBefore(Entry<K,V> existingEntry) {  
    after  = existingEntry;  
    before = existingEntry.before;  
    before.after = this;  
    after.before = this;  
}

  到此爲止,咱們分析了在LinkedHashMap中put一條鍵值對的完整過程。總的來講,相比HashMap而言,LinkedHashMap在向哈希表添加一個鍵值對的同時,也會將其鏈入到它所維護的雙向鏈表中,以便設定迭代順序。

LinkedHashMap 的擴容操做 : resize()

在HashMap中,咱們知道隨着HashMap中元素的數量愈來愈多,發生碰撞的機率將愈來愈大,所產生的子鏈長度就會愈來愈長,這樣勢必會影響HashMap的存取速度。

爲了保證HashMap的效率,系統必需要在某個臨界點進行擴容處理,該臨界點就是HashMap中元素的數量在數值上等於threshold(table數組長度*加載因子)。

可是,不得不說,擴容是一個很是耗時的過程,由於它須要從新計算這些元素在新table數組中的位置並進行復制處理。因此,若是咱們可以提早預知HashMap中元素的個數,那麼在構造HashMap時預設元素的個數可以有效的提升HashMap的性能。

一樣的問題也存在於LinkedHashMap中,由於LinkedHashMap原本就是一個HashMap,只是它還將全部Entry節點鏈入到了一個雙向鏈表中。LinkedHashMap徹底繼承了HashMap的resize()方法,只是對它所調用的transfer方法進行了重寫。咱們先看resize()方法源碼:

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;

    // 若 oldCapacity 已達到最大值,直接將 threshold 設爲 Integer.MAX_VALUE
    if (oldCapacity == MAXIMUM_CAPACITY) {  
        threshold = Integer.MAX_VALUE;
        return;             // 直接返回
    }

    // 不然,建立一個更大的數組
    Entry[] newTable = new Entry[newCapacity];

    //將每條Entry從新哈希到新的數組中
    transfer(newTable);  //LinkedHashMap對它所調用的transfer方法進行了重寫

    table = newTable;
    threshold = (int)(newCapacity * loadFactor);  // 從新設定 threshold
}

  從上面代碼中咱們能夠看出,Map擴容操做的核心在於重哈希。所謂重哈希是指從新計算原HashMap中的元素在新table數組中的位置並進行復制處理的過程。鑑於性能和LinkedHashMap自身特色的考量,LinkedHashMap對重哈希過程(transfer方法)進行了重寫,源碼以下:

/**
 * Transfers all entries to new table array.  This method is called
 * by superclass resize.  It is overridden for performance, as it is
 * faster to iterate using our linked list.
 */
void transfer(HashMap.Entry[] newTable) {
    int newCapacity = newTable.length;
    // 與HashMap相比,藉助於雙向鏈表的特色進行重哈希使得代碼更加簡潔
    for (Entry<K,V> e = header.after; e != header; e = e.after) {
        int index = indexFor(e.hash, newCapacity);   // 計算每一個Entry所在的桶
        // 將其鏈入桶中的鏈表
        e.next = newTable[index];
        newTable[index] = e;   
    }
}

  如上述源碼所示,LinkedHashMap藉助於自身維護的雙向鏈表輕鬆地實現了重哈希操做。

LinkedHashMap 的讀取實現 :get(Object key)

  相對於LinkedHashMap的存儲而言,讀取就顯得比較簡單了。LinkedHashMap中重寫了HashMap中的get方法,源碼以下:

public V get(Object key) {
    // 根據key獲取對應的Entry,若沒有這樣的Entry,則返回null
    Entry<K,V> e = (Entry<K,V>)getEntry(key); 
    if (e == null)      // 若不存在這樣的Entry,直接返回
        return null;
    e.recordAccess(this);
    return e.value;
}

/**
     * Returns the entry associated with the specified key in the
     * HashMap.  Returns null if the HashMap contains no mapping
     * for the key.
     * 
     * HashMap 中的方法
     *     
     */
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

  在LinkedHashMap的get方法中,經過HashMap中的getEntry方法獲取Entry對象。注意這裏的recordAccess方法,若是鏈表中元素的排序規則是按照插入的前後順序排序的話,該方法什麼也不作;若是鏈表中元素的排序規則是按照訪問的前後順序排序的話,則將e移到鏈表的末尾處,筆者會在後文專門闡述這個問題。

另外,一樣地,調用LinkedHashMap的get(Object key)方法後,若返回值是 NULL,則也存在以下兩種可能:

該 key 對應的值就是 null;
HashMap 中不存在該 key。

LinkedHashMap 存取小結

LinkedHashMap的存取過程基本與HashMap基本相似,只是在細節實現上稍有不一樣,這是由LinkedHashMap自己的特性所決定的,由於它要額外維護一個雙向鏈表用於保持迭代順序。

在put操做上,雖然LinkedHashMap徹底繼承了HashMap的put操做,可是在細節上仍是作了必定的調整,好比,在LinkedHashMap中向哈希表中插入新Entry的同時,還會經過Entry的addBefore方法將其鏈入到雙向鏈表中。

在擴容操做上,雖然LinkedHashMap徹底繼承了HashMap的resize操做,可是鑑於性能和LinkedHashMap自身特色的考量,LinkedHashMap對其中的重哈希過程(transfer方法)進行了重寫。在讀取操做上,LinkedHashMap中重寫了HashMap中的get方法,經過HashMap中的getEntry方法獲取Entry對象。在此基礎上,進一步獲取指定鍵對應的值。

LinkedHashMap 與 LRU(Least recently used,最近最少使用)算法

  到此爲止,咱們已經分析完了LinkedHashMap的存取實現,這與HashMap大致相同。LinkedHashMap區別於HashMap最大的一個不一樣點是,前者是有序的,然後者是無序的。爲此,LinkedHashMap增長了兩個屬性用於保證順序,分別是雙向鏈表頭結點header和標誌位accessOrder。

咱們知道,header是LinkedHashMap所維護的雙向鏈表的頭結點,而accessOrder用於決定具體的迭代順序。實際上,accessOrder標誌位的做用可不像咱們描述的這樣簡單,咱們接下來仔細分析一波~

咱們知道,當accessOrder標誌位爲true時,表示雙向鏈表中的元素按照訪問的前後順序排列,能夠看到,雖然Entry插入鏈表的順序依然是按照其put到LinkedHashMap中的順序,但put和get方法均有調用recordAccess方法(put方法在key相同時會調用)。

recordAccess方法判斷accessOrder是否爲true,若是是,則將當前訪問的Entry(put進來的Entry或get出來的Entry)移到雙向鏈表的尾部(key不相同時,put新Entry時,會調用addEntry,它會調用createEntry,該方法一樣將新插入的元素放入到雙向鏈表的尾部,既符合插入的前後順序,又符合訪問的前後順序,由於這時該Entry也被訪問了);

當標誌位accessOrder的值爲false時,表示雙向鏈表中的元素按照Entry插入LinkedHashMap到中的前後順序排序,即每次put到LinkedHashMap中的Entry都放在雙向鏈表的尾部,這樣遍歷雙向鏈表時,Entry的輸出順序便和插入的順序一致,這也是默認的雙向鏈表的存儲順序。

所以,當標誌位accessOrder的值爲false時,雖然也會調用recordAccess方法,但不作任何操做。

put操做與標誌位accessOrder

/ 將key/value添加到LinkedHashMap中      
public V put(K key, V value) {      
    // 若key爲null,則將該鍵值對添加到table[0]中。      
    if (key == null)      
        return putForNullKey(value);      
    // 若key不爲null,則計算該key的哈希值,而後將其添加到該哈希值對應的鏈表中。      
    int hash = hash(key.hashCode());      
    int i = indexFor(hash, table.length);      
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {      
        Object k;      
        // 若key對已經存在,則用新的value取代舊的value     
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {      
            V oldValue = e.value;      
            e.value = value;      
            e.recordAccess(this);      
            return oldValue;      
        }      
    }      

    // 若key不存在,則將key/value鍵值對添加到table中      
    modCount++;    
    //將key/value鍵值對添加到table[i]處    
    addEntry(hash, key, value, i);      
    return null;      
}

  從上述源碼咱們能夠看到,當要put進來的Entry的key在哈希表中已經在存在時,會調用Entry的recordAccess方法;當該key不存在時,則會調用addEntry方法將新的Entry插入到對應桶的單鏈表的頭部。咱們先來看recordAccess方法:

/**
* This method is invoked by the superclass whenever the value
* of a pre-existing entry is read by Map.get or modified by Map.set.
* If the enclosing Map is access-ordered, it moves the entry
* to the end of the list; otherwise, it does nothing.
*/
void recordAccess(HashMap<K,V> m) {  
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;  
    //若是鏈表中元素按照訪問順序排序,則將當前訪問的Entry移到雙向循環鏈表的尾部,  
    //若是是按照插入的前後順序排序,則不作任何事情。  
    if (lm.accessOrder) {  
        lm.modCount++;  
        //移除當前訪問的Entry  
        remove();  
        //將當前訪問的Entry插入到鏈表的尾部  
        addBefore(lm.header);  
      }  
  }

  LinkedHashMap重寫了HashMap中的recordAccess方法(HashMap中該方法爲空),當調用父類的put方法時,在發現key已經存在時,會調用該方法;當調用本身的get方法時,也會調用到該方法。

該方法提供了LRU算法的實現,它將最近使用的Entry放到雙向循環鏈表的尾部。也就是說,當accessOrder爲true時,get方法和put方法都會調用recordAccess方法使得最近使用的Entry移到雙向鏈表的末尾;當accessOrder爲默認值false時,從源碼中能夠看出recordAccess方法什麼也不會作。咱們反過頭來,再看一下addEntry方法:

/**
* This override alters behavior of superclass put method. It causes newly
* allocated entry to get inserted at the end of the linked list and
* removes the eldest entry if appropriate.

LinkedHashMap中的addEntry方法
*/
void addEntry(int hash, K key, V value, int bucketIndex) {

//建立新的Entry,並插入到LinkedHashMap中  
    createEntry(hash, key, value, bucketIndex);  // 重寫了HashMap中的createEntry方法

    //雙向鏈表的第一個有效節點(header後的那個節點)爲最近最少使用的節點,這是用來支持LRU算法的
    Entry<K,V> eldest = header.after;  
    //若是有必要,則刪除掉該近期最少使用的節點,  
    //這要看對removeEldestEntry的覆寫,因爲默認爲false,所以默認是不作任何處理的。  
    if (removeEldestEntry(eldest)) {  
        removeEntryForKey(eldest.key);  
    } else {  
        //擴容到原來的2倍  
        if (size >= threshold)  
            resize(2 * table.length);  
    }  
} 

void createEntry(int hash, K key, V value, int bucketIndex) { 
    // 向哈希表中插入Entry,這點與HashMap中相同 
    //建立新的Entry並將其鏈入到數組對應桶的鏈表的頭結點處, 
    HashMap.Entry<K,V> old = table[bucketIndex];  
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  
    table[bucketIndex] = e;     

    //在每次向哈希表插入Entry的同時,都會將其插入到雙向鏈表的尾部,  
    //這樣就按照Entry插入LinkedHashMap的前後順序來迭代元素(LinkedHashMap根據雙向鏈表重寫了迭代器)
    //同時,新put進來的Entry是最近訪問的Entry,把其放在鏈表末尾 ,也符合LRU算法的實現  
    e.addBefore(header);  
    size++;  
}

  一樣是將新的Entry鏈入到table中對應桶中的單鏈表中,但能夠在createEntry方法中看出,同時也會把新put進來的Entry插入到了雙向鏈表的尾部。
  
從插入順序的層面來講,新的Entry插入到雙向鏈表的尾部能夠實現按照插入的前後順序來迭代Entry,而從訪問順序的層面來講,新put進來的Entry又是最近訪問的Entry,也應該將其放在雙向鏈表的尾部。在上面的addEntry方法中還調用了removeEldestEntry方法,該方法源碼以下:

/**
 * Returns <tt>true</tt> if this map should remove its eldest entry.
 * This method is invoked by <tt>put</tt> and <tt>putAll</tt> after
 * inserting a new entry into the map.  It provides the implementor
 * with the opportunity to remove the eldest entry each time a new one
 * is added.  This is useful if the map represents a cache: it allows
 * the map to reduce memory consumption by deleting stale entries.
 *
 * <p>Sample use: this override will allow the map to grow up to 100
 * entries and then delete the eldest entry each time a new entry is
 * added, maintaining a steady state of 100 entries.
 * <pre>
 *     private static final int MAX_ENTRIES = 100;
 *
 *     protected boolean removeEldestEntry(Map.Entry eldest) {
 *        return size() > MAX_ENTRIES;
 *     }
 * </pre>
 *
 * <p>This method typically does not modify the map in any way,
 * instead allowing the map to modify itself as directed by its
 * return value.  It <i>is</i> permitted for this method to modify
 * the map directly, but if it does so, it <i>must</i> return
 * <tt>false</tt> (indicating that the map should not attempt any
 * further modification).  The effects of returning <tt>true</tt>
 * after modifying the map from within this method are unspecified.
 *
 * <p>This implementation merely returns <tt>false</tt> (so that this
 * map acts like a normal map - the eldest element is never removed).
 *
 * @param    eldest The least recently inserted entry in the map, or if
 *           this is an access-ordered map, the least recently accessed
 *           entry.  This is the entry that will be removed it this
 *           method returns <tt>true</tt>.  If the map was empty prior
 *           to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
 *           in this invocation, this will be the entry that was just
 *           inserted; in other words, if the map contains a single
 *           entry, the eldest entry is also the newest.
 * @return   <tt>true</tt> if the eldest entry should be removed
 *           from the map; <tt>false</tt> if it should be retained.
 */
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

}

  該方法是用來被重寫的,通常地,若是用LinkedHashmap實現LRU算法,就要重寫該方法。好比能夠將該方法覆寫爲若是設定的內存已滿,則返回true,這樣當再次向LinkedHashMap中putEntry時,在調用的addEntry方法中便會將近期最少使用的節點刪除掉(header後的那個節點)。在第七節,筆者便重寫了該方法並實現了一個名副其實的LRU結構。

get操做與標誌位accessOrder

public V get(Object key) {
    // 根據key獲取對應的Entry,若沒有這樣的Entry,則返回null
    Entry<K,V> e = (Entry<K,V>)getEntry(key); 
    if (e == null)      // 若不存在這樣的Entry,直接返回
        return null;
    e.recordAccess(this);
    return e.value;
}

  在LinkedHashMap中進行讀取操做時,同樣也會調用recordAccess方法。上面筆者已經表述的很清楚了,此不贅述。

LinkedListMap與LRU小結

  使用LinkedHashMap實現LRU的必要前提是將accessOrder標誌位設爲true以便開啓按訪問順序排序的模式。咱們能夠看到,不管是put方法仍是get方法,都會致使目標Entry成爲最近訪問的Entry,所以就把該Entry加入到了雙向鏈表的末尾:get方法經過調用recordAccess方法來實現;

put方法在覆蓋已有key的狀況下,也是經過調用recordAccess方法來實現,在插入新的Entry時,則是經過createEntry中的addBefore方法來實現。這樣,咱們便把最近使用的Entry放入到了雙向鏈表的後面。屢次操做後,雙向鏈表前面的Entry即是最近沒有使用的,這樣當節點個數滿的時候,刪除最前面的Entry(head後面的那個Entry)便可,由於它就是最近最少使用的Entry。

使用LinkedHashMap實現LRU算法

  以下所示,筆者使用LinkedHashMap實現一個符合LRU算法的數據結構,該結構最多能夠緩存6個元素,但元素多餘六個時,會自動刪除最近最久沒有被使用的元素,以下所示:

public class LRU<K,V> extends LinkedHashMap<K, V> implements Map<K, V>{

    private static final long serialVersionUID = 1L;

    public LRU(int initialCapacity,
             float loadFactor,
                        boolean accessOrder) {
        super(initialCapacity, loadFactor, accessOrder);
    }

    /** 
     * @description 重寫LinkedHashMap中的removeEldestEntry方法,當LRU中元素多餘6個時,
     *              刪除最不常用的元素
     * @author rico       
     * @created 2017年5月12日 上午11:32:51      
     * @param eldest
     * @return     
     * @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)     
     */  
    @Override
    protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
        // TODO Auto-generated method stub
        if(size() > 6){
            return true;
        }
        return false;
    }

    public static void main(String[] args) {

        LRU<Character, Integer> lru = new LRU<Character, Integer>(
                16, 0.75f, true);

        String s = "abcdefghijkl";
        for (int i = 0; i < s.length(); i++) {
            lru.put(s.charAt(i), i);
        }
        System.out.println("LRU中key爲h的Entry的值爲: " + lru.get('h'));
        System.out.println("LRU的大小 :" + lru.size());
        System.out.println("LRU :" + lru);
    }
}

  下圖是程序的運行結果:

LinkedHashMap 有序性原理分析

如前文所述,LinkedHashMap 增長了雙向鏈表頭結點header 和 標誌位accessOrder兩個屬性用於保證迭代順序。可是要想真正實現其有序性,還差臨門一腳,那就是重寫HashMap 的迭代器,其源碼實現以下:

private abstract class LinkedHashIterator<T> implements Iterator<T> {
    Entry<K,V> nextEntry    = header.after;
    Entry<K,V> lastReturned = null;

    /**
     * The modCount value that the iterator believes that the backing
     * List should have.  If this expectation is violated, the iterator
     * has detected concurrent modification.
     */
    int expectedModCount = modCount;

    public boolean hasNext() {         // 根據雙向列表判斷 
            return nextEntry != header;
    }

    public void remove() {
        if (lastReturned == null)
        throw new IllegalStateException();
        if (modCount != expectedModCount)
        throw new ConcurrentModificationException();

            LinkedHashMap.this.remove(lastReturned.key);
            lastReturned = null;
            expectedModCount = modCount;
    }

    Entry<K,V> nextEntry() {        // 迭代輸出雙向鏈表各節點
        if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
            if (nextEntry == header)
                throw new NoSuchElementException();

            Entry<K,V> e = lastReturned = nextEntry;
            nextEntry = e.after;
            return e;
    }
}

// Key 迭代器,KeySet
private class KeyIterator extends LinkedHashIterator<K> {   
    public K next() { return nextEntry().getKey(); }
}

   // Value 迭代器,Values(Collection)
private class ValueIterator extends LinkedHashIterator<V> {
    public V next() { return nextEntry().value; }
}

// Entry 迭代器,EntrySet
private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
    public Map.Entry<K,V> next() { return nextEntry(); }
}

  從上述代碼中咱們能夠知道,LinkedHashMap重寫了HashMap 的迭代器,它使用其維護的雙向鏈表進行迭代輸出。
 

JDK1.8的改動

原文是基於JDK1.6的實現,實際上JDK1.8對其進行了改動。
首先它刪除了addentry,createenrty等方法(事實上是hashmap的改動影響了它而已)。

linkedhashmap一樣使用了大部分hashmap的增刪改查方法。
新版本linkedhashmap主要是經過對hashmap內置幾個方法重寫來實現lru的。

hashmap不提供實現:

void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }

linkedhashmap的實現:

處理元素被訪問後的狀況

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

處理元素插入後的狀況

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

處理元素被刪除後的狀況

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;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}
}

另外1.8的hashmap在鏈表長度超過8時自動轉爲紅黑樹,會按順序插入鏈表中的元素,能夠自定義比較器來定義節點的插入順序。

1.8的linkedhashmap一樣會使用這一特性,當變爲紅黑樹之後,節點的前後順序一樣是插入紅黑樹的順序,其雙向鏈表的性質沒有改表,只是原來hashmap的鏈表變成了紅黑樹而已,在此不要混淆。

總結

本文從linkedhashmap的數據結構,以及源碼分析,到最後的LRU緩存實現,比較深刻地剖析了linkedhashmap的底層原理。
總結如下幾點:

1 linkedhashmap在hashmap的數組加鏈表結構的基礎上,將全部節點連成了一個雙向鏈表。

2 當主動傳入的accessOrder參數爲false時, 使用put方法時,新加入元素不會被加入雙向鏈表,get方法使用時也不會把元素放到雙向鏈表尾部。

3 當主動傳入的accessOrder參數爲true時,使用put方法新加入的元素,若是遇到了哈希衝突,而且對key值相同的元素進行了替換,就會被放在雙向鏈表的尾部,當元素超過上限且removeEldestEntry方法返回true時,直接刪除最先元素以便新元素插入。若是沒有衝突直接放入,一樣加入到鏈表尾部。使用get方法時會把get到的元素放入雙向鏈表尾部。

4 linkedhashmap的擴容比hashmap來的方便,由於hashmap須要將原來的每一個鏈表的元素分別在新數組進行反向插入鏈化,而linkedhashmap的元素都連在一個鏈表上,能夠直接迭代而後插入。

5 linkedhashmap的removeEldestEntry方法默認返回false,要實現lru很重要的一點就是集合滿時要將最久未訪問的元素刪除,在linkedhashmap中這個元素就是頭指針指向的元素。實現LRU能夠直接實現繼承linkedhashmap並重寫removeEldestEntry方法來設置緩存大小。jdk中實現了LRUCache也能夠直接使用。

參考文章
http://cmsblogs.com/?p=176

https://www.jianshu.com/p/8f4f58b4b8ab

https://blog.csdn.net/wang_8101/article/details/83067860

https://www.cnblogs.com/create-and-orange/p/11237072.html

https://www.cnblogs.com/ganchuanpu/p/8908093.html

微信公衆號

我的公衆號:程序員黃小斜


黃小斜是 985 碩士,阿里巴巴Java工程師,在自學編程、技術求職、Java學習等方面有豐富經驗和獨到看法,但願幫助到更多想要從事互聯網行業的程序員們。

做者專一於 JAVA 後端技術棧,熱衷於分享程序員乾貨、學習經驗、求職心得,以及自學編程和Java技術棧的相關乾貨。

黃小斜是一個斜槓青年,堅持學習和寫做,相信終身學習的力量,但願和更多的程序員交朋友,一塊兒進步和成長!

原創電子書:
關注微信公衆號【程序員黃小斜】後回覆【原創電子書】便可領取我原創的電子書《菜鳥程序員修煉手冊:從技術小白到阿里巴巴Java工程師》這份電子書總結了我2年的Java學習之路,包括學習方法、技術總結、求職經驗和麪試技巧等內容,已經幫助不少的程序員拿到了心儀的offer!

程序員3T技術學習資源: 一些程序員學習技術的資源大禮包,關注公衆號後,後臺回覆關鍵字 「資料」 便可免費無套路獲取,包括Java、python、C++、大數據、機器學習、前端、移動端等方向的技術資料。

技術公衆號:Java技術江湖

若是你們想要實時關注我更新的文章以及分享的乾貨的話,能夠關注個人微信公衆號【Java技術江湖】

這是一位阿里 Java 工程師的技術小站。做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

Java工程師必備學習資源:
關注公衆號後回覆」Java「便可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源

個人公衆號

相關文章
相關標籤/搜索