死磕 java集合之LinkedHashMap源碼分析

来源:https://www.cnblogs.com/tong-yuan/archive/2019/04/01/10639263.html
-Advertisement-
Play Games

死磕 java集合之LinkedHashMap源碼分析 你瞭解它的存儲結構嗎? 你知道它為什麼可以用來實現LRU緩存嗎? 它真的可以直接拿來實現LRU緩存嗎? ...


歡迎關註我的公眾號“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢游源碼的海洋。

簡介

LinkedHashMap內部維護了一個雙向鏈表,能保證元素按插入的順序訪問,也能以訪問順序訪問,可以用來實現LRU緩存策略。

LinkedHashMap可以看成是 LinkedList + HashMap。

繼承體系

LinkedHashMap

LinkedHashMap繼承HashMap,擁有HashMap的所有特性,並且額外增加了按一定順序訪問的特性。

存儲結構

LinkedHashMap-structure

我們知道HashMap使用(數組 + 單鏈表 + 紅黑樹)的存儲結構,那LinkedHashMap是怎麼存儲的呢?

通過上面的繼承體系,我們知道它繼承了HashMap,所以它的內部也有這三種結構,但是它還額外添加了一種“雙向鏈表”的結構存儲所有元素的順序。

添加刪除元素的時候需要同時維護在HashMap中的存儲,也要維護在LinkedList中的存儲,所以性能上來說會比HashMap稍慢。

源碼解析

屬性

/**
* 雙向鏈表頭節點 
*/
transient LinkedHashMap.Entry<K,V> head;

/**
* 雙向鏈表尾節點 
*/
transient LinkedHashMap.Entry<K,V> tail;

/**
* 是否按訪問順序排序 
*/
final boolean accessOrder;

(1)head

雙向鏈表的頭節點,舊數據存在頭節點。

(2)tail

雙向鏈表的尾節點,新數據存在尾節點。

(3)accessOrder

是否需要按訪問順序排序,如果為false則按插入順序存儲元素,如果是true則按訪問順序存儲元素。

內部類

// 位於LinkedHashMap中
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);
    }
}

// 位於HashMap中
static class Node<K, V> implements Map.Entry<K, V> {
    final int hash;
    final K key;
    V value;
    Node<K, V> next;
}

存儲節點,繼承自HashMap的Node類,next用於單鏈表存儲於桶中,before和after用於雙向鏈表存儲所有元素。

構造方法

public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

public LinkedHashMap() {
    super();
    accessOrder = false;
}

public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

前四個構造方法accessOrder都等於false,說明雙向鏈表是按插入順序存儲元素。

最後一個構造方法accessOrder從構造方法參數傳入,如果傳入true,則就實現了按訪問順序存儲元素,這也是實現LRU緩存策略的關鍵。

afterNodeInsertion(boolean evict)方法

在節點插入之後做些什麼,在HashMap中的putVal()方法中被調用,可以看到HashMap中這個方法的實現為空。

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

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

evict,驅逐的意思。

(1)如果evict為true,且頭節點不為空,且確定移除最老的元素,那麼就調用HashMap.removeNode()把頭節點移除(這裡的頭節點是雙向鏈表的頭節點,而不是某個桶中的第一個元素);

(2)HashMap.removeNode()從HashMap中把這個節點移除之後,會調用afterNodeRemoval()方法;

(3)afterNodeRemoval()方法在LinkedHashMap中也有實現,用來在移除元素後修改雙向鏈表,見下文;

(4)預設removeEldestEntry()方法返回false,也就是不刪除元素。

afterNodeAccess(Node<K,V> e)方法

在節點訪問之後被調用,主要在put()已經存在的元素或get()時被調用,如果accessOrder為true,調用這個方法把訪問到的節點移動到雙向鏈表的末尾。

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    // 如果accessOrder為true,並且訪問的節點不是尾節點
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        // 把p節點從雙向鏈表中移除
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        
        if (a != null)
            a.before = b;
        else
            last = b;
        
        // 把p節點放到雙向鏈表的末尾
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        // 尾節點等於p
        tail = p;
        ++modCount;
    }
}

(1)如果accessOrder為true,並且訪問的節點不是尾節點;

(2)從雙向鏈表中移除訪問的節點;

(3)把訪問的節點加到雙向鏈表的末尾;(末尾為最新訪問的元素)

afterNodeRemoval(Node<K,V> e)方法

在節點被刪除之後調用的方法。

void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    // 把節點p從雙向鏈表中刪除。
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

經典的把節點從雙向鏈表中刪除的方法。

get(Object key)方法

獲取元素。

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

如果查找到了元素,且accessOrder為true,則調用afterNodeAccess()方法把訪問的節點移到雙向鏈表的末尾。

總結

(1)LinkedHashMap繼承自HashMap,具有HashMap的所有特性;

(2)LinkedHashMap內部維護了一個雙向鏈表存儲所有的元素;

(3)如果accessOrder為false,則可以按插入元素的順序遍歷元素;

(4)如果accessOrder為true,則可以按訪問元素的順序遍歷元素;

(5)LinkedHashMap的實現非常精妙,很多方法都是在HashMap中留的鉤子(Hook),直接實現這些Hook就可以實現對應的功能了,並不需要再重寫put()等方法;

(6)預設的LinkedHashMap並不會移除舊元素,如果需要移除舊元素,則需要重寫removeEldestEntry()方法設定移除策略;

(7)LinkedHashMap可以用來實現LRU緩存淘汰策略;

彩蛋

LinkedHashMap如何實現LRU緩存淘汰策略呢?

首先,我們先來看看LRU是個什麼鬼。LRU,Least Recently Used,最近最少使用,也就是優先淘汰最近最少使用的元素。

如果使用LinkedHashMap,我們把accessOrder設置為true是不是就差不多能實現這個策略了呢?答案是肯定的。請看下麵的代碼:

package com.coolcoding.code;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author: tangtong
 * @date: 2019/3/18
 */
public class LRUTest {
    public static void main(String[] args) {
        // 創建一個只有5個元素的緩存
        LRU<Integer, Integer> lru = new LRU<>(5, 0.75f);
        lru.put(1, 1);
        lru.put(2, 2);
        lru.put(3, 3);
        lru.put(4, 4);
        lru.put(5, 5);
        lru.put(6, 6);
        lru.put(7, 7);
    
        System.out.println(lru.get(4));
    
        lru.put(6, 666);
    
        // 輸出: {3=3, 5=5, 7=7, 4=4, 6=666}
        // 可以看到最舊的元素被刪除了
        // 且最近訪問的4被移到了後面
        System.out.println(lru);
    }
}

class LRU<K, V> extends LinkedHashMap<K, V> {

    // 保存緩存的容量
    private int capacity;
    
    public LRU(int capacity, float loadFactor) {
        super(capacity, loadFactor, true);
        this.capacity = capacity;
    }
    
    /**
    * 重寫removeEldestEntry()方法設置何時移除舊元素
    * @param eldest
    * @return 
    */
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // 當元素個數大於了緩存的容量, 就移除元素
        return size() > this.capacity;
    }
}

歡迎關註我的公眾號“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢游源碼的海洋。

qrcode


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 流,確定是筆者內心很嚮往的天堂,有他之後JAVA在處理數據就變更加的靈動。加上lambda表達不喜歡都不行。JAVA8也為流在提供另一個功能——並行流。即是有並行流,那麼是不是也有順序流。沒有錯。我前面操作的一般都是順序流。在JAVA8裡面並行流和順序流是可以轉變的。來看一個例子——筆者列印數字。 ...
  • #!/usr/bin/env python# -*- coding:utf-8 -*-# 1.簡述解釋型語言和編譯型語言的區別?"""1.解釋型語言:Python,PHP,Ruby.特點是一行一行的解釋,一行一行的傳輸給電腦,報錯行前面可以執行.2.編譯型語言:C,C++,Java,C#,Go.特 ...
  • slice 是 Go 語言一個很重要的數據結構。網上已經有很多文章寫過了,似乎沒必要再寫。但是每個人看問題的視角不同,寫出來的東西自然也不一樣。我這篇會從更底層的彙編語言去解讀它,這是一個新的世界。 ...
  • 線程簡介 什麼是線程 現代操作系統調度的最小單元是線程,也叫輕量級進程(Light Weight Process),在一個進程里可以創建多個線程,這些線程都擁有各自的計數器、堆棧和局部變數等屬性,並且能夠訪問共用的記憶體變數。 線程生命周期 java.lang.Thread.State 中定義了 6  ...
  • 目錄: 一、JDK安裝 1.1、JDK下載 1.2、環境變數配置 1.3、測試 二、ANDROID-SDK安裝 2.1、下載 2.2、環境變數配置 三、Flutter安裝 3.1、下載 3.2、環境變數配置 3.3、測試 四、IDE安裝 4.1、下載 4.2、插件安裝 五、錯誤解決 5.1、Andr ...
  • 早在介紹多態的時候,曾經提到公雞實例的性別屬性可能被篡改為雌性,不過面向對象的三大特性包含了封裝、繼承和多態,只要把性別屬性設置為private私有級別,也不提供setSex這樣的性別修改方法,那麼性別屬性就被嚴嚴實實地封裝了起來,不但外部無法修改性別屬性,連公雞類的子類都無法修改。如此一來,公雞實 ...
  • 1.不要在常量和變數中出現易混淆的數字 個人感覺這條在於編程命名的規範性。代碼除了給機器看,也要給人看。要寫能夠結構清晰,命名規範,讓人看懂的代碼。 字母l作為長整型標誌時務必大寫 L 2.莫讓常量蛻變成變數 java的常量有編譯期常量和運行期常量。他們都被static final修飾。引用被sta ...
  • 1.JVM運行時數據區 (1)程式計數器:線程私有,可以看做是當前線程所執行的位元組碼的行號指示器。選取下一條位元組碼指令、分支、線程恢復等都需要程式計數器來完成。 (2)虛擬機棧:同樣是線程私有,它描述的是java方法執行的記憶體模型:每個方法在執行的同時,都會創建一個棧幀,用來存放局部變數表、操作數棧 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...