HashMap在Go與Java的底層實現與區別

在Java中

在Java中hash表的底層數據結構與擴容等已經是面試集合類問題中幾乎必問的點了。網上有對源碼的解析已經非常詳細了我們這里還是說說其底層實現。

基礎架構

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {private static final long serialVersionUID = 362498820763181265L;// 默認的初始容量是16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;// 最大容量static final int MAXIMUM_CAPACITY = 1 << 30;// 默認的負載因子static final float DEFAULT_LOAD_FACTOR = 0.75f;// 當桶(bucket)上的結點數大于等于這個值時會轉成紅黑樹static final int TREEIFY_THRESHOLD = 8;// 當桶(bucket)上的結點數小于等于這個值時樹轉鏈表static final int UNTREEIFY_THRESHOLD = 6;// 桶中結構轉化為紅黑樹對應的table的最小容量static final int MIN_TREEIFY_CAPACITY = 64;// 存儲元素的數組,總是2的冪次倍transient Node<k,v>[] table;// 一個包含了映射中所有鍵值對的集合視圖transient Set<map.entry<k,v>> entrySet;// 存放元素的個數,注意這個不等于數組的長度。transient int size;// 每次擴容和更改map結構的計數器transient int modCount;// 閾值(容量*負載因子) 當實際大小超過閾值時,會進行擴容int threshold;// 負載因子final float loadFactor;
}

本文所有Java代碼均來自JavaGuide(HashMap 源碼分析 | JavaGuide),這里主要就是定義一些必要的常量,被用于哈希表的創建參數,擴容參數等待。

然后就是hash表中的Node節點的數據結構,我們的k-v鍵值對就存儲在一個Node類里面。在jdk1.7前其實與redis中的字典Dictionary數據結構中的hash表十分類似,即采用線性搜索和拉鏈法。在jdk1.8 及以后版本1中,添加了樹化,即當節點數大于8就會將當前節點轉化為紅黑樹,這樣做的目的主要是為了增加搜索效率,紅黑樹的時間復雜度為O(log n)如果沒有樹化鏈表查詢的時間復雜度為O(n) 。接下來就看看JavaGuide中給出的節點類:

鏈表節點:

// 繼承自 Map.Entry<K,V>
static class Node<K,V> implements Map.Entry<K,V> {final int hash;// 哈希值,存放元素到hashmap中時用來與其他元素hash值比較final K key;//鍵V value;//值// 指向下一個節點Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey()        { return key; }public final V getValue()      { return value; }public final String toString() { return key + "=" + value; }// 重寫hashCode()方法public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}// 重寫 equals() 方法public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}
}

樹節點:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // 父TreeNode<K,V> left;    // 左TreeNode<K,V> right;   // 右TreeNode<K,V> prev;    // needed to unlink next upon deletionboolean red;           // 判斷顏色TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}// 返回根節點final TreeNode<K,V> root() {for (TreeNode<K,V> r = this, p;;) {if ((p = r.parent) == null)return r;r = p;}

resize()

然后我們來重點講一講resize()擴容這個方法。

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {// 超過最大值就不再擴充了,就只好隨你碰撞去吧if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 沒超過最大值,就擴充為原來的2倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in threshold// 創建對象時初始化容量大小放在threshold中,此時只需要將其作為新的數組容量newCap = oldThr;else {// signifies using defaults 無參構造函數創建的對象在這里計算容量和閾值newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {// 創建時指定了初始化容量或者負載因子,在這里進行閾值初始化,// 或者擴容前的舊容量小于16,在這里計算新的resize上限float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null) {// 把每個bucket都移動到新的buckets中for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)// 只有一個節點,直接計算元素新的位置即可newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)// 將紅黑樹拆分成2棵子樹,如果子樹節點數小于等于 UNTREEIFY_THRESHOLD(默認為 6),則將子樹轉換為鏈表。// 如果子樹節點數大于 UNTREEIFY_THRESHOLD,則保持子樹的樹結構。((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else {Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;// 原索引if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}// 原索引+oldCapelse {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 原索引放到bucket里if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 原索引+oldCap放到bucket里if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
}

在Java的hashmap中,在jdk1.8以后是通過負載因子的判斷來選擇是否進行resize()方法默認負載因子是0.75。如果存儲數據與當前容量之比為0.75就會進行擴容。同時當我們存儲數據過多時,無論我們的hash算法做了什么樣的優化,一定還是會有hash沖突的存在,所以為了解決沖突,我們使用拉鏈法。在插入數據時,我們采用的方式是將已存在hashmap中的鍵值對的值與插入的值進行比較,如果二者相等就進行覆蓋,如果二者不相等就使用尾加法加載鏈表尾部(在redis5中,使用的是equals()進行比較,因為redis存儲的所有值都是String字符串。)。我們將一個拉鏈稱之為一個哈希桶。但是我們試想如果一個桶的節點過于多了,那么我們查找時遍歷起來是很花費時間的,在hashtable中使用的是線性搜索法加拉鏈法來解決這個問題的,但是如果我們一直盲目使用線性搜索法的話,(我們暫且將線性搜索法占據的hash表數組槽位與hash表當前容量的比值稱為線性搜索負載因子)當線性搜索負載因子過大時,我們的hash表的查找效率會受到極大的影響。所有在jdk1.8后的樹化則很好解決了這個問題,即當拉鏈上的節點樹大于8時,會先對數組容量進行判斷,如果小于64先擴容(hashmap擴容都為2倍擴容),否則進行拉鏈法。(為什么先擴容呢?因為hashmap的hash函數計算與容量有關所以擴容后會得到新的hash值,避免了hash沖突,相較于紅黑樹的遍歷,我們肯定更優先考慮的是這種做法)

在Golang中

基礎架構

type hmap struct {count intflags uint8B uint8noverflow uint16hash0 uint32buckets unsafe.Pointeroldbuckets unsafe.Pointernevacuate uintptrextra *mapextra
}type mapextra struct {overflow *[]*bmapoldoverflow *[]*bmapnextOverflow *bmap
}
  • count 表示當前hash表中的元素。

  • B 表示當前hash表持有的 buckets (即桶數組)數量,由于hash表中桶數量都為2的倍數,所以該字段會存儲對數。(這里和redis的hash表一樣,在redis的rehash過程中,需要先創建一個2倍舊數組長度的新數組,然后進行hash桶遷移)。

  • oldbuckets是哈希表在擴容時用于保存之前buckets的字段,它的大小是當前buckets的一半。

在go的hashmap中,我們使用溢出桶來降低擴容頻率,本質上就是預先分配幾個數組空間用于存儲超出容量的k-v

擴容方法

  • 開放尋址法

    • 其實就是線性搜索法。簡而言之就是依次探測和比較數組中的元素以判斷目標鍵值對是否存在于哈希表,當我們向當前哈希表寫入新數據時,如果發生了沖突,就會將鍵值對寫入下一個索引不為空的位置。開放尋址法中對性能影響最大的是裝載因子,它是數組中元素數量與數組大小的比值。隨著裝 載因子的增加,線性探測的平均用時會逐漸增加,這會影響哈希表的讀寫性能。當裝載率超過70% 之后,哈希表的性能就會急劇下降,而一旦裝載率達到100%,整個哈希表就會完全失效,這時查找 和插入任意元素的時間復雜度都是O(n),我們需要遍歷數組中的全部元素,所以在實現哈希表時一 定要關注裝載因子的變化。

  • 拉鏈法

    • 和jdk 1.7 一樣,就不做過多解釋。

在go中的k-v添加我們需要注意的是當插入的k-v小于25時會以如下方式插入:

hash := make(map[string]int, 3)
hash["1"] = 1
hash["2"] = 2
hash["3"] = 3

如若大于25個,就會分別創建兩個數組,分別存儲k 和 v,然后以遍歷形式插入。

言歸正傳,在go的hashmap中擴容條件為:裝在因子超過6.5 || 哈希表使用太多溢出桶。

同時由于在go的hashmap的擴容不是原子性的所以需要判斷以避免二次擴容(這和redis也一樣,增刪改查需要判斷當前數據庫的hash表是否在進行rehash)。

擴容數據結構

說了這么多,接下來我們就來重點介紹go的hashmap擴容的數據結構變化。runtime. evacuate會將一個舊桶中的數據分流到兩個新桶,所以它會創建兩個用于保存分配 上下文的runtime.evacDst結構體,這兩個結構體分別指向了一個新桶。

func evacuate(t *maptype, h *hmap, oldbucket uintptr) {b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))newbit := h.noldbuckets()if !evacuated(b) {var xy [2]evacDstx := &xy[0]x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))x.k = add(unsafe.Pointer(x.b), dataOffset)x. v = add(x.k, bucketCnt*uintptr(t.keysize))y := &xy[1Jy. b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))y.k = add(unsafe.Pointer(y.b), dataOffset)y.v = add(y.k, bucketCnt*uintptr(t.keysize))
}

go中的hashamp擴容分為等量擴容和翻倍擴容,如果是前者就只初始化一個桶,如果是翻倍擴容,就會初始化兩個桶。會把一個鏈表數據分到新表兩個位置,將8個節點分流到兩個桶中(這里獲取桶位置采用取模或者位運算來得到數據存儲的桶位置),然后將k的指針指向兩個桶位置。

在數據查詢時,會先判斷是否在進行分流,如果在進行,就先會從舊桶中讀取數據。相較于Java的hashmap它不會一次性將所有的元素重新哈希,而是在每次插入元素時,都會將一部分元素移動到新的桶中。這樣可以避免一次性的大量計算,但可能會導致一段時間內的查詢效率稍低。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/18200.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/18200.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/18200.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

簡單幾步構建設企業流媒體服務器

簡單幾步構建設企業流媒體服務器 在企業應用中&#xff0c;涉及到視頻服務時&#xff0c;直接的應用要求即是視頻的實時查看&#xff01;如果使用各大平臺的流媒體服務&#xff0c;對于針對設備的視頻服務&#xff0c;如IPC的各種應用場景&#xff0c;在這個卷的時代&#xff…

Cesium For Unity 在Unity中無法下載的問題

Unity 下載失敗&#xff0c;提供百度網盤“com.cesium.unity-1.10.0.tgz”下載鏈接 鏈接&#xff1a;https://pan.baidu.com/s/1PybXQ8EvkRofOKD6rSN66g?pwd1234 提取碼&#xff1a;1234 導入方法&#xff1a; 1.打開PackageManager;Window-PackageManager 2.在PackageMan…

從機械塵埃到智能星河:探索從工業心臟到AI大腦的世紀跨越(一點個人感想)...

全文預計1400字左右&#xff0c;預計閱讀需要8分鐘。 近期&#xff0c;人工智能領域呈現出前所未有的活躍景象&#xff0c;各類創新成果如雨后春筍般涌現&#xff0c;不僅推動了科技的邊界&#xff0c;也為全球經濟注入了新的活力。 這不&#xff0c;最近報道16家國內外企業在A…

優思學院:質量工程師必備技能清單,你具備了嗎?

想要了解質量工程師需要具備哪些技能和知識&#xff0c;最直接且實際的方法就是分析招聘廣告中的關鍵詞&#xff0c;這比道聽途說更加有效。為此&#xff0c;優思學院搜集了大量關于質量工程師職位的招聘信息&#xff0c;并為大家進行詳細分析。我們通常選擇中高級職位進行分析…

嵌入式C語言指針詳細解說

各位伙伴大家好,在實現操作系統的控制的時候,經常需要使用到指針,利用這次詳細分析一下指針的用法。 C語言指針真正精髓的地方在于指針可以進行加減法,這一點極大的提升了程序對指針使用的靈活性,同時也帶來了不小的學習負擔。正是因為C語言指針可運算,才奠定了如今C語言…

「Element-UI表頭添加帶Icon的提示信息」

一、封裝全局組件 &#x1f353; 注意&#xff1a;可以直接復制該文件 <!-- // 寫一個PromptMessage的組件&#xff0c;并全局注冊 --> <template><div class"tooltip"><el-tooltip effect"dark" placement"right">&l…

MySQL select for update 加鎖

背景 當多人操作同一個客戶下賬號的時候&#xff0c;希望順序執行&#xff0c;某個時刻只有一個人在操作&#xff1b;當然可以通過引入redis這種中間件實現&#xff0c;但考慮到并發不會很多&#xff0c;所以不想再引入別的中間件。 表結構 create table jiankunking_accoun…

基于Python flask的豆瓣電影數據分析可視化系統,功能多,LSTM算法+注意力機制實現情感分析,準確率高達85%

研究背景 隨著數字化時代的到來&#xff0c;電影產業正迎來新的發展機遇和挑戰。基于Python Flask的豆瓣電影數據分析可視化系統的研究背景凸顯了對電影數據的深度分析和情感挖掘的需求。該系統功能豐富&#xff0c;不僅實現了多樣化的數據分析功能&#xff0c;還結合了LSTM算…

2024/5/23 學習雜記

目錄 位運算與邏輯運算讀程序練習 在switchcase 語句中能否使用continue關鍵字&#xff1f;為什么&#xff1f; 為什么盡量不使用goto語句? void i與i i和i 哪個效率更高&#xff1f; 良好的條件比較語句風格 memcpy memset 位運算與邏輯運算讀程序練習 int x 3, y…

如何解決Redis緩存擊穿?

Redis緩存擊穿問題,也稱作熱點Key問題,通常發生在高并發場景下,當一個被高并發訪問且緩存重建業務較復雜的key突然失效時,大量請求會同時訪問數據庫,導致數據庫壓力瞬間增大。以下是解決Redis緩存擊穿問題的幾種方案: 使用鎖(互斥鎖): 原理:當緩存失效時,不是所有線…

CTF| 格式化字符串漏洞

格式化字符串漏洞是PWN題常見的考察點&#xff0c;僅次于棧溢出漏洞。漏洞原因&#xff1a;程序使用了格式化字符串作為參數&#xff0c;并且格式化字符串為用戶可控。其中觸發格式化字符串漏洞函數主要是printf、sprintf、fprintf、prin等C庫中print家族的函數 0x01 格式化字符…

雙非二本找工作前的準備day28

學習目標&#xff1a; 每天復習代碼隨想錄上的題目2-3道算法&#xff08;時間充足可以繼續&#xff09; 今日碎碎念&#xff1a; 1&#xff09;進入貪心與dp專題&#xff0c;過完準備二刷&#xff0c;以及刷劍指offer。 2&#xff09;這兩天沒更新是休息一下&#xff0c;然后…

如何深入理解、應用及擴展 Twemproxy?no.15

Twemproxy 架構及應用 Twemproxy 是 Twitter 的一個開源架構&#xff0c;它是一個分片資源訪問的代理組件。如下圖所示&#xff0c;它可以封裝資源池的分布及 hash 規則&#xff0c;解決后端部分節點異常后的探測和重連問題&#xff0c;讓 client 訪問盡可能簡單&#xff0c;同…

C語言之宏詳解(超級詳細!)

目錄 一、用宏前須知-#define相關知識 大致結構&#xff1a; 對預定義符號的補充&#xff1a; 二、用#define定義宏 什么是宏&#xff1f; #define的替換規則&#xff1a; 三、常用的宏定義 1、宏定義常量 2、定義一個宏語句 3、宏定義函數 宏與函數的對比&#xff1a; …

29【PS 作圖】宮燈 夜景轉換

夜景轉化 1 原圖 2 選中要變換的圖層,然后點擊“顏色查找” 再3DLUT文件中,選擇moonlight.3DL,可以快速把圖層變成偏夜景的顏色 結果如下: 3 選擇“曲線” 把曲線 右邊往上調【亮的更亮】,左邊往下調【暗的更暗】 4 添加燈光 新建一個圖層

前端面試題大合集8----性能優化篇

一、哪些方法可以提升網站前端性能 1、Http請求優化 主要分為減少Http請求次數&#xff0c;減小請求數據量和緩存三方面。 減少Http請求次數&#xff0c;可以通過以下方法實現&#xff1a; 合并js、css文件&#xff1b;使用css-spirites技術合并圖片&#xff1b;壓縮圖片大…

HTML+CSS+JS簡易計算器

HTMLCSSJS簡易計算器 index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>簡易計算器</t…

AAA實驗配置

一、實驗目的 掌握AAA本地認證的配置方法 掌握AAA本地授權的配置方法 掌握AAA維護的方法 1.搭建實驗拓撲圖 2.完成基礎配置&#xff1a; 3.使用ping命令測試兩臺設備的連通性&#xff1a; 二、配置AAA 1.打開R1&#xff1a;配置AAA方案 這兩個方框內的可以改名&#xff0c…

百度頁面奔跑的白熊html、css

一、相關知識-動畫 1.基本使用&#xff1a;先定義再調用 2. 調用動畫 用keyframes定義動畫&#xff08;類似定義類選擇器&#xff09; keyframes動畫名稱{ 0%{ width:100px&#xff1b; } 100%{ width:200px; } } 使用動畫 div { width:200px; height:200px; background-…

前端面試題日常練-day28 【面試題】

題目 希望這些選擇題能夠幫助您進行前端面試的準備&#xff0c;答案在文末。 1. 在Vue中&#xff0c;以下哪個選項用于監聽組件生命周期鉤子函數&#xff1f; a) watch b) computed c) lifecycle d) created 2. 在Vue中&#xff0c;以下哪個選項用于在列表渲染時為每個元素…