詳細解讀Java中Map集合的底層原理(乾貨+源碼解讀)

来源:https://www.cnblogs.com/qian-fen/archive/2023/05/25/17432481.html
-Advertisement-
Play Games

本文將為大家詳細講解Java中的Map集合,這是我們進行開發時經常用到的知識點,也是大家在學習Java中很重要的一個知識點,更是我們在面試時有可能會問到的問題。文章較長,乾貨滿滿,建議大家收藏慢慢學習。文末有本文重點總結,主頁有全系列文章分享。技術類問題,歡迎大家和我們一起交流討論! ...


本文將為大家詳細講解Java中的Map集合,這是我們進行開發時經常用到的知識點,也是大家在學習Java中很重要的一個知識點,更是我們在面試時有可能會問到的問題。

文章較長,乾貨滿滿,建議大家收藏慢慢學習。文末有本文重點總結,主頁有全系列文章分享。技術類問題,歡迎大家和我們一起交流討論!

前言

在上一篇文章中給大家講解了Java里的Set集合及其常用子類。現在我們已經掌握了Java里的兩大集合,最後還有另一大集合等待著我們學習,這就是Map集合。與之前的集合不太一樣,Map集合屬於雙列集合,該集合中的信息是key-value形式;而之前的LIst和Set都是單列集合,裡面的元素沒有key。

有些小伙伴可能會很好奇,我們已經學習了List和Set集合了,為什麼還要再搞出來一個Map集合呢?Map集合與Collection集合又有什麼不同呢? 要想搞清楚以上問題,我們可以考慮這麼一個需求。

假如我們利用List集合,來存儲一份Student學生信息,要求你根據name來查找某個指定的學生分數,你要怎麼實現?按照我們之前的經驗,常用的方式就是遍歷這個List,並挨個判斷name是否相等,找到後就返回指定元素。

其實這種需求非常的常見,通過一個關鍵詞來查詢對應的值。如果我們使用List來實現該需求,效率會非常的低,因為平均需要掃描完集合的一半元素才能確定。而如果使用Map這種key-value鍵值對映射表的數據結構,就可以通過key快速地查找到value值。


全文大約【8500】 字,不說廢話,只講可以讓你學到技術、明白原理的純乾貨!本文帶有豐富的案例及配圖視頻,讓你更好地理解和運用文中的技術概念,並可以給你帶來具有足夠啟迪的思考......

一. Map集合

1. 簡介

Map集合是一種以鍵值對形式存儲和操作數據的數據結構,建立了key-value之間的映射關係,常用於存儲和處理複雜的數據。同時Map也是一種雙列集合介面,它有多個實現類,包括HashMap、TreeMap、LinkedHashMap等,最常用的是HashMap類。其中,HashMap是按哈希演算法來實現存取鍵對象的,這是我們開發時最常用的Map子類,而TreeMap類則可以對鍵對象進行排序。

image.png

Map集合中的每個元素,都包含了一個鍵(key)和一個值(value),key和value組成了鍵-值的映射表,我們稱其為鍵值對。鍵用於唯一標識一個元素,值用於存儲該元素的數據,一般情況下,這個key和value可以是任何引用類型的數據。其中,鍵key是無序、無下標、不重覆的,最多只能有一個key為null。值value是無序、無下標、可重覆的,可以有多個value為null

並且這個key和value之間是單向的一對一關係,即通過指定的key,總能找到唯一的、確定的value。當我們想從Map中取出數據時,只要給出指定的key,就能取出對應的value。

配套視頻如下 :戳我直達視頻

2. 特點

根據上面我們對Map概念的講解,把Map的主要特點給大家總結如下:

  • Map List 不同,Map是一種雙列集合;
  • Map 存儲的是 key-value 的映射關係;
  • Map 不保證順序 。在遍歷時,遍歷的順序不一定是 put() 時放入的 key 的順序,也不一定是 key 的排序順序。

3. 實現方式

在Java中,Map集合的實現方式主要有兩種:基於哈希表和基於樹結構。接下來給大家簡單介紹一下基於這兩種結構的Map集合。

3.1 基於哈希表的Map集合

基於哈希表的Map,其底層是基於哈希表作為數據結構的集合,主要的實現類是HashMap

HashMap中,每個元素都包含一個鍵和一個值。當我們在添加元素時,HashMap會根據鍵的哈希值計算出對應的桶(Bucket),並將元素存儲到該桶中。如果不同的鍵具有相同的哈希值,就會出現哈希衝突,此時HashMap會使用鏈表或紅黑樹等數據結構來解決衝突。基於哈希表的Map集合具有快速的添加、刪除和查找元素的特點,但元素的順序是不確定的。

3. 實現方式

在Java中,Map集合的實現方式主要有兩種:基於哈希表和基於樹結構。接下來壹哥給大家簡單介紹一下基於這兩種結構的Map集合。

3.1 基於哈希表的Map集合

基於哈希表的Map,其底層是基於哈希表作為數據結構的集合,主要的實現類是HashMap。在HashMap中,每個元素都包含一個鍵和一個值。當我們在添加元素時,HashMap會根據鍵的哈希值計算出對應的桶(Bucket),並將元素存儲到該桶中。如果不同的鍵具有相同的哈希值,就會出現哈希衝突,此時HashMap會使用鏈表或紅黑樹等數據結構來解決衝突。基於哈希表的Map集合具有快速的添加、刪除和查找元素的特點,但元素的順序是不確定的。

3.2 基於樹結構的Map集合

基於樹結構的Map集合,其底層是基於紅黑樹作為數據結構的集合,主要的實現類是TreeMap。在TreeMap中,每個元素也都包含一個鍵和一個值。我們在添加元素時,TreeMap會根據鍵的比較結果,將元素存儲到正確的位置上,使得元素可以按照鍵的升序或降序排列。基於樹結構的Map集合,具有快速查找和遍歷元素的特點,但添加和刪除元素的速度較慢。

4. 常用方法

Map集合的使用和其他集合類似,主要包括添加、刪除、獲取、遍歷元素等操作。

當我們調用put(K key, V value)方法時,會把key和value進行映射並放入Map。當調用V get(K key)時,可以通過key獲取到對應的value;如果key不存在,則返回null。如果我們只是想查詢某個key是否存在,可以調用containsKey(K key)方法。另外我們也可以通過 Map提供的keySet()方法,獲取所有key組成的集合,進而遍歷Map中所有的key-value對。

下表中就是Map里的一些常用方法,大家可以記一下,這些方法在Map的各實現子類中也都存在。

方法 描述
clear() 刪除Map中所有的鍵-值對
isEmpty() 判斷Map是否為空
size() 計算Map中鍵/值對的數量
put() 將鍵/值對添加到Map中
putAll() 將所有的鍵/值對添加到Map中
putIfAbsent() 如果Map中不存在指定的鍵,則將指定的鍵/值對插入到Map中。
remove() 刪除Map中指定鍵key的映射關係
containsKey() 檢查Map中是否存在指定key對應的映射關係。
containsValue() 檢查Map中是否存在指定的 value對應的映射關係。
replace() 替換Map中指定key對應的value。
replaceAll() 將Map中所有的映射關係替換成給定函數執行的結果。
get() 獲取指定 key 對應對 value
getOrDefault() 獲取指定 key 對應對 value,如果找不到 key ,則返回設置的預設值
forEach() 對Map中的每個映射執行指定的操作。
entrySet() 返回Map中所有的鍵值對
keySet() 返回Map中所有的key鍵
values() 返回Map中所有的value值
merge() 添加鍵值對到Map中
compute() 對Map中指定key的值進行重新計算
computeIfAbsent() 對Map中指定key的值進行重新計算,如果該key不存在,則添加到Map中
computeIfPresent() 當key存在該Map時,對Map中指定key的值進行重新計算

與本節內容配套的視頻鏈接如下: 戳我直達視頻課程

5. 常用實現類

Java中有多個Map介面的實現類,接下來就給大家逐一簡單介紹一下。

image.png

5.1 HashMap

HashMap是Java中最常用的Map集合實現類,它基於哈希表實現,具有快速查找鍵值對的優點。 HashMap的存儲方式是無序的,也就是說,遍歷HashMap集合時,得到的鍵值對順序是不確定的。下麵是創建HashMap集合的代碼示例:

Map<String, Integer> hashMap = new HashMap<>();

5.2 TreeMap

TreeMap是Java中另一個常用的Map集合實現類,它基於紅黑樹實現,具有自動排序鍵值對的優點。TreeMap的存儲方式是有序的,也就是說,遍歷TreeMap集合時得到的鍵值對,是按照鍵的自然順序或指定比較器的順序排序的。下麵是創建TreeMap集合的代碼示例:

Map<String, Integer> treeMap = new TreeMap<>();

5.3 LinkedHashMap

LinkedHashMap是Java中另一個Map集合實現類,它繼承自HashMap,並保持了插入順序。也就是說,遍歷LinkedHashMap集合時,得到的鍵值對的順序是按照插入順序排序的。下麵是創建LinkedHashMap集合的代碼示例:

Map<String, Integer> linkedHashMap = new LinkedHashMap<>();

5.4 Hashtable

Hashtable是Java中另一個Map集合實現類,它與HashMap非常相似,但Hashtable是線程安全的。Hashtable的存儲方式是無序的,也就是說,遍歷Hashtable集合時,得到的鍵值對的順序是不確定的。下麵是創建Hashtable集合的代碼示例:

Map<String, Integer> hashtable = new Hashtable<>();

需要註意的是,由於Hashtable是線程安全的,所以在多線程環境中使用Hashtable的性能可能會較低,現在一般是建議使用ConcurrentHashMap。

5.5 ConcurrentHashMap

ConcurrentHashMap是Java中的另一個Map集合實現類,它與Hashtable非常相似,但是ConcurrentHashMap是線程安全的,並且性能更高。ConcurrentHashMap的存儲方式是無序的,也就是說,遍歷ConcurrentHashMap集合時,得到的鍵值對的順序是不確定的。下麵是創建ConcurrentHashMap集合的代碼示例:

Map<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();

需要註意的是,雖然ConcurrentHashMap是線程安全的,但仍然需要註意多線程環境下的併發問題

二. HashMap集合

1. 簡介

HashMap繼承自AbstractMap類,實現了Map、Cloneable、java.io.Serializable介面,如下圖所示:

image.png

HashMap是基於散列表實現的雙列集合,它存儲的是key-value鍵值對映射,每個key-value鍵值對也被稱為一條Entry條目。其中的 key與 value,可以是任意的數據類型,其類型可以相同也可以不同。但一般情況下,key都是String類型,有時候也可以使用Integer類型;value可以是任何類型。並且在HashMap中,最多只能有一個記錄的key為null,但可以有多個value的值為null。

HashMap中這些鍵值對(Entry)會分散存儲在一個數組當中,這個數組就是HashMap的主體。 預設情況下,HashMap這個數組中的每個元素的初始值都是null。但HashMap中最多只允許一條記錄的key為null,允許多條記錄的value為null。

另外HashMap是非線程安全的,即任一時刻如果有多個線程同時對HashMap進行寫操作,有可能會導致數據的不一致。 如果需要滿足線程的安全性要求,可以用 Collections.synchronizedMap() 方法使得HashMap具有線程安全的能力,或者使用ConcurrentHashMap來替代。

HashMap實現了Map介面,會根據key的hashCode值存儲數據,具有較快的訪問速度。另外HashMap是無序的,不會記錄插入元素的順序。我們一般是根據key的hashCode值來存取數據, 大多數情況下都可以直接根據key來定位到它所對應的值,因而HashMap有較快的訪問速度,但遍歷順序卻不確定。

2. 特點

根據上面的描述,把HashMap的核心特點總結如下:

  • 基於哈希表實現,具有快速查找鍵值對的優點;
  • HashMap的存儲方式是無序的;
  • HashMap的key-value可以是任意類型,但key一般都是String類型,value類型任意;
  • HashMap最多只能有一個記錄的key為null,但可以有多個value為null。

3. 常用操作

HashMap的使用方法和其他Map集合類似,主要包括添加元素、刪除元素、獲取元素、遍歷元素等操作。接下來會詳細地給大家介紹HashMap集合的常用操作。

3.1 創建Map集合對象

創建HashMap對象的方式有多種,我們可以使用構造函數,代碼如下:

// 使用構造函數創建HashMap對象 
Map<String, Integer> map1 = new HashMap<>();

3.2 添加元素

向HashMap集合添加元素的方式和List集合類似,也是使用put()方法,下麵是向HashMap集合中添加元素的代碼示例:

public class Demo14 {

	public static void main(String[] args) {
		//HashMap
		Map<String, String> map = new HashMap<>();
		map.put("name","一一哥");
        //map.put("name","壹哥");
		map.put("age", "30");
		map.put("sex", "男");
		System.out.println("map="+map);
	}
}

看到上面的代碼,有些小伙伴可能會問,如果我們往同一個Map中存儲兩個相同的key,但分別放入不同的value,這會有什麼問題嗎?

其實我們在Map中,重覆地放入 key-value 並不會有任何問題,但一個 key 只能關聯一個 value 因為當我們調用put(K key, V value)方法時,如果放入的key已經存在,put()方法會返回被刪除的舊value,否則就會返回null。所以Map中不會有重覆的key,因為放入相同的key時,就會把原有key對應的value給替換掉。

3.3 刪除元素

從HashMap集合中刪除元素的方式也和List集合類似,使用remove()方法。下麵是從HashMap集合中刪除元素的代碼示例:

//從HashMap集合中移除元素
map.remove("age");

3.4 獲取元素

從HashMap集合中獲取元素的方式和List集合不同,需要使用鍵來獲取值。HashMap集合提供了兩種方法來獲取值,一種是使用get()方法,另一種是使用getOrDefault()方法。如果指定的鍵不存在,使用get()方法會返回null,而getOrDefault()方法則會返回指定的預設值。下麵是從HashMap集合中獲取元素的代碼示例:

public class Demo15 {

	public static void main(String[] args) {
		//HashMap
		Map<String, String> map = new HashMap<>();
		map.put("name","一一哥");
		map.put("age", "30");
		map.put("sex", "男");
		
		//根據key獲取指定的元素
		String name = map.get("name");
		String age = map.get("age");
		//根據key獲取指定的元素,並設置預設值
		String sex = map.getOrDefault("sex","男");
		String height = map.getOrDefault("height","0");
		System.out.println("[name]="+name+",[age]="+age+",[sex]="+sex+",[height]="+height);
	}
}

3.5 遍歷元素

遍歷HashMap集合的方式和List集合不同,需要使用迭代器或者foreach迴圈遍歷鍵值對,下麵是遍歷HashMap集合的代碼示例:

/**
 * @author 一一哥Sun
 */
public class Demo16 {

	public static void main(String[] args) {
		//HashMap
		Map<String, String> map = new HashMap<>();
		map.put("name","一一哥");
		map.put("age", "30");
		map.put("sex", "男");
		
		//遍歷方式一:使用迭代器遍歷HashMap 
		//獲取集合中的entry條目集合
		Set<Entry<String, String>> entrySet = map.entrySet();
		//獲取集合中攜帶的Iterator迭代器對象
		Iterator<Map.Entry<String, String>> iterator = entrySet.iterator(); 
		//通過迴圈進行迭代遍歷
		while (iterator.hasNext()) {     
			//獲取每一個Entry條目對象
		    Map.Entry<String, String> entry = iterator.next();    
		    //獲取條目中的key
		    String key = entry.getKey();    
		    //獲取條目中的value
		    String value = entry.getValue();     
		    System.out.println(key + " = " + value); 
		} 

		//遍歷方式二:用foreach迴圈遍歷HashMap 
		for (Map.Entry<String, String> entry : map.entrySet()) {    
		    String key = entry.getKey();     
		    String value = entry.getValue();     
		    System.out.println(key + " = " + value); 
		}
	}
}

大家要註意,當我們使用Map時,任何依賴順序的邏輯都是不可靠的。比如,我們存入"A","B","C" 3個key,遍歷時,每個key會保證被遍歷一次且僅遍歷一次,但遍歷的順序完全沒有保證,甚至對於不同的JDK版本,相同的代碼遍歷輸出的順序都可能是不同的!所以我們在 遍歷Map時,要註意輸出的key是無序的!

3.6 判斷Map集合是否為空

判斷HashMap集合是否為空可以使用isEmpty()方法,如果Map集合為空,則返回true,否則返回false。下麵是判斷HashMap集合是否為空的代碼示例:

// 判斷HashMap是否為空 
boolean isEmpty = map.isEmpty(); 

3.7 獲取Map集合的大小

獲取HashMap集合的大小可以使用size()方法,返回HashMap集合中鍵值對的數量,下麵是獲取HashMap集合大小的代碼示例:

// 獲取HashMap的大小 
int size = map.size(); 

3.8 判斷Map集合是否包含指定的鍵或值

如果我們想判斷HashMap集合是否包含指定的鍵或值,可以使用containsKey()和containsValue()方法。如果Map集合包含指定的鍵或值,則返回true,否則返回false。下麵是判斷HashMap集合是否包含指定鍵或值的代碼示例:

// 判斷HashMap是否包含指定鍵
boolean containsKey = map.containsKey("name"); 

// 判斷HashMap是否包含指定值 
boolean containsValue = map.containsValue("一一哥"); 

3.9 替換Map集合中的鍵值對

如果我們想替換HashMap集合中的鍵值對,可以使用replace()方法將指定鍵的值替換成新的值。如果指定的鍵不存在,則不進行任何操作。下麵是替換HashMap集合中的鍵值對的代碼示例:

// 替換HashMap中的鍵值對 
map.replace("name", "壹哥"); 

3.10 合併兩個Map集合

如果我們想合併兩個HashMap集合,可以使用putAll()方法,將另一個HashMap集合中所有的鍵值對,添加到當前的HashMap集合中。下麵是合併兩個HashMap集合的代碼示例:

public class Demo16 {

	public static void main(String[] args) {
		//HashMap
		Map<String, String> map1 = new HashMap<>();
		map1.put("name","一一哥");
		map1.put("age", "30");
		map1.put("sex", "男");
		
		// 創建另一個TreeMap集合 
		Map<String, String> map2 = new HashMap<>(); 
		map2.put("height", "180"); 
		map2.put("salary", "5w"); 

		//將map1中的鍵值對添加到map2中 
		map2.putAll(map1); 
		System.out.println("map2="+map2);
	}
}

3.11 獲取Map集合中所有的鍵或值

如果我們想獲取HashMap集合中所有的鍵或值,可以分別使用keySet()和values()方法。這兩個方法會返回一個Set集合和一個Collection集合,它們包含了HashMap集合中所有的鍵或值。下麵是獲取HashMap集合中所有鍵或值的代碼示例:

public class Demo18 {

	public static void main(String[] args) {
		//HashMap
		Map<String, String> map = new HashMap<>();
		map.put("name","一一哥");
		map.put("age", "30");
		map.put("sex", "男");
		
		// 獲取HashMap中所有的鍵 
		Set<String> keySet = map.keySet(); 
		for(String key : keySet) {
			System.out.println("key="+key); 
		}

		// 獲取HashMap中所有的值 
		Collection<String> values = map.values(); 
		for(String value:values) {
			System.out.println("value"+value); 
		}
	}
}

4. 原理概述(重點)

作為開發時最常用的Map集合,HashMap在我們面試時被問到的概率非常高,尤其是關於其原理、數據結構、衝突解決、併發、擴容等相關的內容,更是經常被問到。

如果我們想要瞭解HashMap的底層原理,首先得知道HashMap的底層數據結構,而這個數據結構在不同的JDK版本中是不同的。我們可以把HashMap的底層數據結構分為兩大版本,即JDK 7及其以前的版本 和 JDK 8及其以後的版本。 大家註意,本文主要是結合JDK 8的源碼,給大家講解HashMap的底層原理。

  • 在JDK 7及其以前版本的HashMap中,其底層的數據結構是 數組+鏈表
  • 而從JDK 8開始則採用 數組+鏈表+紅黑樹 的數據結構,其中的 數組是Entry類型或者說是Node類型數組

5. hash衝突解決機制

因為HashMap底層會使用Hash演算法來處理數據的存取,當數據非常多時就有一定的概率出現hash衝突,其解決過程如下。

  • 當我們往HashMap中存儲數據時,首先會利用hash(key)方法 計算出key的hash值,再利用該 hash值HashMap數組的長度-1 進行 與運算,從而得到該key在數組中對應的下標位置
  • 如果該位置上目前還沒有存儲數據,則直接將該key-value鍵值對數據存入到數組中的這個位置;
  • 如果該位置目前已經有了數據,則把新的數據存入到一個鏈表中;
  • 當鏈表的長度超過閾值(JDK 8中該值為8)時,會將鏈表轉換為紅黑樹(轉換為紅黑樹還需要滿足其他的條件,鏈表長度達到閾值只是其中的一個條件)。

image.png

通過這樣的機制,HashMap就解決了存值時可能產生的哈希衝突問題,並可以大大提高我們的查找效率。

當然由於HashMap極其重要,它的內容非常多,尤其是原理性的內容更多。但由於篇幅限制,不會在本文中過多地講解HashMap原理等的內容

三. Hashtable集合

1. 簡介

Hashtable也是Java中的一個Map集合,它與HashMap非常相似,但Hashtable是線程安全的,而HashMap不是線程安全的。Hashtable也可以存儲鍵值對,並可以通過鍵快速查找到對應的值,Hashtable的鍵和值也都可以是任何類型的對象。

因為Hashtable是線程安全的,因此適用於多線程環境。在多線程環境中,如果需要對Hashtable進行多個操作,需要使用synchronized關鍵字來保證線程安全。但需要我們註意的是,在多線程環境中使用Hashtable可能會影響性能,所以如果不需要保證線程安全,請儘量使用HashMap。

Hashtable集合的底層結構主要是數組+鏈表,數組是Entry數組,鏈表也是用Entry來實現的 所以Hashtable的底層核心,其實也是基於哈希表,它使用哈希函數將鍵映射到哈希表中的一個位置,從而實現快速查找。另外Hashtable的存儲方式是無序的,也就是說,遍歷Hashtable集合得到的鍵值對的順序是不確定的。

2. 常用方法

Hashtable與HashMap類似,它的常用方法也與HashMap一樣:

  • put(K key, V value):將指定的鍵值對存儲到Hashtable中。
  • get(Object key):返回指定鍵所對應的值,如果不存在該鍵則返回null。
  • remove(Object key):從Hashtable中移除指定鍵所對應的鍵值對。
  • containsKey(Object key):判斷Hashtable中是否包含指定的鍵。
  • containsValue(Object value):判斷Hashtable中是否包含指定的值。
  • size():返回Hashtable中鍵值對的數量。
  • clear():移除Hashtable中的所有鍵值對。

3. 基本使用

下麵是一個簡單的Hashtable集合示例,演示瞭如何創建Hashtable集合、存儲鍵值對、獲取值、遍歷集合等操作。

import java.util.Hashtable;
import java.util.Map;

public class Demo19 {
    public static void main(String[] args) {
        // 創建Hashtable集合
        Map<String, Integer> hashtable = new Hashtable<>();

        // 存儲鍵值對
        hashtable.put("apple", 10);
        hashtable.put("banana", 20);
        hashtable.put("orange", 30);

        // 獲取值
        int value1 = hashtable.get("apple");
        int value2 = hashtable.get("banana");
        int value3 = hashtable.get("orange");
        System.out.println("apple: " + value1);
        System.out.println("banana: " + value2);
        System.out.println("orange: " + value3);

        // 移除鍵值對
        hashtable.remove("orange");

        // 遍歷集合
        for (Map.Entry<String, Integer> entry : hashtable.entrySet()) {
            String key = entry.getKey();
            int value = entry.getValue();
            System.out.println(key + ": " + value);
        }

        // 清空集合
		hashtable.clear(); 
    }
}

其他方法的使用與HashMap基本一致,就不再一一細說了。

四. ConcurrentHashMap集合

1. 簡介

由於在多線程環境下,常規的HashMap可能會在數組擴容及重哈希時出現 死迴圈、臟讀 等線程安全問題,雖然有HashTable、Collections.synchronizedMap()可以取代HashMap進行併發操作,但因它們都是利用一個 全局的synchronized鎖 來同步不同線程之間的併發訪問,因此性能較差。

所以Java就從JDK 1.5版本開始,在J.U.C(java.util.concurrent併發包) 引入了一個高性能的併發操作集合---ConcurrentHashMap(可以簡稱為CHM)。該集合是一種線程安全的哈希表實現,相比Hashtable和SynchronizedMap,在多線程場景下具有更好的性能和可伸縮性

並且ConcurrentHashMap集合在讀數據時不需要加鎖,寫數據時會加鎖,但鎖的粒度較小,不會對整個集合加鎖。而其內部又大量的利用了 volatile,final,CAS等lock-free(無鎖併發)技術,減少了鎖競爭對於性能的影響,具有更好的寫併發能力,但降低了對讀一致性的要求。 因此既保證了併發操作的安全性,又確保了讀、寫操作的高性能,可以說它的併發設計與實現都非常的精巧。

另外ConurrentHashMap中的key與value都不能為null,否則會產生空指針異常!

2. ConcurrentHashMap類關係

ConcurrentHashMap與HashMap具有相同的父類AbstractMap,他倆可以說是“親兄弟”,所以ConcurrentHashMap在一般的特性和使用上與HashMap基本是一致的,甚至很多底層原理也是相似的。

但兩者所在的包是不同的,ConcurrentHashMap是在java.util.concurrent包中,HashMap是在java.util包中,我們可以參考下圖:

image.png

以上所述的ConcurrentHashMap概念及特征,是不區分版本的,但實際上不同版本的ConcurrentHashMap內部實現差異很大,所以面試時經常會被問到不同版本之間的差異、各自特征。接下來會針對JDK 7 與 JDK 8 這兩個經典版本分別進行闡述。

3. 實現原理

3.1 併發控制機制

在JDK 7中,ConcurrentHashMap的核心機制是分段鎖(Segment),每個Segment內部維護了一個哈希表,且這個哈希表是線程安全的。而ConcurrentHashMap中的每個操作,都是先對所在的Segment進行加鎖,然後再執行具體的操作。

當多個線程對不同的Segment進行操作時,它們之間是併發的。當多個線程對同一個Segment進行操作時,它們會競爭鎖,但不會影響到其他Segment的操作。這種機制有效地降低了鎖的粒度,提高了併發訪問效率。

ConcurrentHashMap的另一個優點是支持可伸縮性。當需要增加ConcurrentHashMap的容量時,我們只需要增加Segment的數量即可,這種機制使得ConcurrentHashMap在高併發場景下具有良好的可伸縮性。

3.2 JDK 7版本的ConcurrentHashMap

在JDK 7版本中,ConcurrentHashMap和HashMap的設計思路其實是差不多的,但為了支持併發操作,做了一定的改進。比如ConcurrentHashMap中的數據是一段一段存放的,我們把這些分段稱之為Segment分段,在每個Segment分段中又有 數組+鏈表 的數據結構。

預設情況下ConcurrentHashMap把主幹分成了 16個Segment分段,並對每一段都單獨加鎖,我們把這種設計策略稱之為 "分段鎖"ConcurrentHashMap就是利用這種分段鎖機制進行併發控制的。JDK 7中ConcurrentHashMap的基本數據結構如下圖所示:

image.png

在理想狀態下,ConcurrentHashMap可以 同時支持16個線程 執行併發寫操作,以及任意數量的線程進行併發讀操作。在寫操作時,通過分段鎖技術,只對所操作的段加鎖而不會影響其它分段,且在讀操作時(幾乎)不需要加鎖。

3.3 JDK 8版本的ConcurrentHashMap

在JDK 8中,ConcurrentHashMap相對於JDK 7版本做了很大的改動,從實現的代碼量上就可以看出來,JDK 7中ConcurrentHashMap不到2000行代碼,而JDK 8中則有6000多行代碼。

JDK 8中放棄了臃腫的Segment設計,取而代之採用了 Node數組 + 鏈表 + 紅黑樹 + CAS + Synchronized 技術來保證併發安全,實現併發控制操作。但是在ConcurrentHashMap的實現中保留了 Segment 的定義,這是為了 保證序列化時的相容性,但並沒有任何結構上的用處。在JDK 8中用synchronized替換ReentrantLock的原因大致如下:

  • 減少記憶體開銷: 如果使用ReentrantLock,就需要節點繼承AQS來獲得同步支持,這增加了記憶體開銷,而JDK 8中只有頭節點需要進行同步。
  • 內部優化: synchronized是JVM直接支持的,JVM能夠在運行時作出相應的優化措施,比如 鎖粗化、鎖消除、鎖自旋等。

4. 基本使用

ConcurrentHashMap支持與HashMap相同的常用操作,如put、get、remove等。下麵介紹一些常用操作。

4.1 插入元素

ConcurrentHashMap的put方法用於向集合中插入一個鍵值對,如果鍵已經存在,則會更新對應的值。下麵是向ConcurrentHashMap中插入元素的示例:

import java.util.concurrent.ConcurrentHashMap;

public class Demo19 {
public static void main(String\[] args) {
// 創建ConcurrentHashMap集合
ConcurrentHashMap\<String, Integer> map = new ConcurrentHashMap<>();

        // 插入元素
        map.put("apple", 10);
        map.put("banana", 20);
        map.put("orange", 30);

        // 輸出元素
        System.out.println(map);
    }

}

根據上面代碼的執行結果可知,ConcurrentHashMap中的鍵值對是無序的。

4.2 獲取元素

ConcurrentHashMap的get方法用於獲取指定鍵對應的值,如果鍵不存在,則返回null。下麵是一個從ConcurrentHashMap中獲取元素的示例:

import java.util.concurrent.ConcurrentHashMap;

public class Demo20 {
    public static void main(String[] args) {
        // 創建ConcurrentHashMap集合
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 插入元素
        map.put("apple", 10);
        map.put("banana", 20);
        map.put("orange", 30);

        // 獲取元素
        Integer value = map.get("apple");
        System.out.println(value);
    }
}

4.3 刪除元素

ConcurrentHashMap的remove方法用於從集合中刪除指定鍵的元素,下麵是一個從ConcurrentHashMap中刪除元素的示例:

import java.util.concurrent.ConcurrentHashMap;

public class Demo21 {
    public static void main(String[] args) {
        // 創建ConcurrentHashMap集合
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 插入元素
        map.put("apple", 10);
        map.put("banana", 20);
        map.put("orange", 30);

        // 刪除元素
        map.remove("apple");

        // 輸出元素
        System.out.println(map);
    }
}

五. 結語

至此,我們就把Map及其子類學習完了,現在你知道Map有什麼特性及其用法了嗎?接下來我們簡單總結一下今天的重點吧:

  • Map是一種映射表,可以通過key快速查找value;
  • 可以通過 for each 遍歷 keySet() ,也可以通過 for each 遍歷 entrySet() ,直接獲取 key-value
  • 最常用的Map是HashMap;
  • Hashtable是線程安全的集合,底層結構主要是數組+鏈表;
  • ConcurrentHashMap可以良好地進行併發處理,是一種線程安全且高效的集合。


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

-Advertisement-
Play Games
更多相關文章
  • **我是 javapub,一名 `Markdown` 程式員從👨‍💻,八股文種子選手。** **面試官: 上個面試官對你的基礎有了一定瞭解,聽說你小子很不錯!下麵我們聊點有深度的。** **面試官: 簡單介紹下 CAS 你瞭解什麼?** **候選人:** CAS是Compare And Swap ...
  • NameServer是一個註冊中心,提供服務註冊和服務發現的功能。NameServer可以集群部署,集群中每個節點都是對等的關係(沒有像ZooKeeper那樣在集群中選舉出一個Master節點),節點之間互不通信。 **服務註冊** Broker啟動的時候會向所有的NameServer節點進行註冊, ...
  • 本篇帶你走進AIGC的基本使用,一步一步註冊ChatGPT,申請自己的API進行使用,解決代理的問題,最後介紹如何本地部署ChatGPT,以及通過免費雲平臺搭建代理轉發,從而不需要使用魔法就可以訪問。 ...
  • JVM(Java虛擬機)是Java程式的運行環境,它可以通過一些系統參數進行配置和優化。以下是一些常用的JVM系統參數: 1. -Xmx: 用於設置JVM堆的最大記憶體大小。例如,-Xmx1g表示將堆的最大大小設置為1GB。 2. -Xms: 用於設置JVM堆的初始記憶體大小。例如,-Xms512m表示 ...
  • 基本數據類型和字元串類型的自動轉換<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ page contentType="text/html;charset=UTF-8" language="j ...
  • [toc] 你好!我是[@馬哥python說](https://www.zhihu.com/people/13273183132),一名10年程式猿,正在試錯用pyecharts開發可視化大屏的非常規排版。 以下,我用8種ThemeType展示的同一個可視化數據大屏,可視化主題是分析**“淄博燒烤” ...
  • 來源:https://www.duidaima.com/Group/Topic/JAVA/11942 ## **1、什麼是狀態機** ### 1.1 什麼是狀態 先來解釋什麼是“狀態”( State )。現實事物是有不同狀態的,例如一個自動門,就有 open 和 closed 兩種狀態。我們通常所說 ...
  • [toc] # 高階函數 高階函數是將函數用作參數或返回值的函數,還可以把函數賦值給一個變數。 所有函數類型都有一個圓括弧括起來的參數類型列表以及一個返回類型:(A, B) -> C 表示接受類型分別為 A 與 B 兩個參數並返回一個 C 類型值的函數類型。 參數類型列表可以為空,如 () -> A ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...