日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

Map再整理,从底层源码探究HashMap

發布時間:2023/11/28 生活经验 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Map再整理,从底层源码探究HashMap 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

本文為對Map集合的再一次整理。內容包括:Map HashMap LinkedHashMap TreeHashMap HashTable ConcurrentHashMap

Map

Map<k,v>使用鍵值對存儲,map會維護與鍵k相關聯的值v。兩個key可以關聯相同的對象,但key不能重復,常見的key是String類型,但也可以是任何對象。通過鍵就可以找到對應的值,這種數據結構就是Map(映射)

Map接口中定義的方法:

void clear() 
//刪除所有的映射
default V compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) 
//嘗試計算指定鍵的映射及其當前映射的值(如果沒有當前映射, null )。  
default V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction) 
/*如果指定的鍵尚未與值相關聯(或映射到 null ),則嘗試使用給定的映射函數計算其值,并將其輸入到此/映射中,除非 null 。 */ 
default V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) 
//如果指定的密鑰的值存在且非空,則嘗試計算給定密鑰及其當前映射值的新映射。  
boolean containsKey(Object key) 
//如果此映射包含指定鍵的映射,則返回 true 。  
boolean containsValue(Object value) 
//如果此map將一個或多個鍵映射到指定的值,則返回 true 。  
Set<Map.Entry<K,V>> entrySet() 
//返回此map中包含的映射的Set視圖。  
boolean equals(Object o) 
//將指定的對象與此映射進行比較以獲得相等性。  
default void forEach(BiConsumer<? super K,? super V> action) 
//對此映射中的每個條目執行給定的操作,直到所有條目都被處理或操作引發異常。  
V get(Object key) 
//返回到指定鍵所映射的值,或 null  
default V getOrDefault(Object key, V defaultValue) 
//返回到指定鍵所映射的值,或 defaultValue
int hashCode() 
//返回此map的哈希碼值。  
boolean isEmpty() 
//如果此map不包含鍵值映射,則返回 true 。  
Set<K> keySet() 
//返回此map中包含的鍵的Set視圖。  
default V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction) 
//如果指定的鍵尚未與值相關聯或與null相關聯,則將其與給定的非空值相關聯。  
V put(K key, V value) 
//將指定的值與該映射中的指定鍵相關聯(可選操作)。  
void putAll(Map<? extends K,? extends V> m) 
//將指定map的所有映射復制到此映射(可選操作)。  
default V putIfAbsent(K key, V value) 
/*如果指定的鍵尚未與某個值相關聯(或映射到 null )將其與給定值相關聯并返回null,否則返回當前值。  */
V remove(Object key) 
//如果存在(從可選的操作),從該map中刪除一個鍵的映射。  
default boolean remove(Object key, Object value) 
//僅當指定的密鑰當前映射到指定的值時刪除該條目。  
default V replace(K key, V value) 
//只有當目標映射到某個值時,才能替換指定鍵的條目。  
default boolean replace(K key, V oldValue, V newValue) 
//僅當當前映射到指定的值時,才能替換指定鍵的條目。  
default void replaceAll(BiFunction<? super K,? super V,? extends V> function) 
//將每個條目的值替換為對該條目調用給定函數的結果,直到所有條目都被處理或該函數拋出異常。  
int size() 
//返回此map中鍵值映射的數量。  
Collection<V> values() 
//返回此map中包含的值的Collection視圖。  

Map的實現類

線程不安全實現類

1.HashMap

JDK1.8 之前 HashMap 底層是 數組和鏈表 結合在一起使用也就是 鏈表散列。HashMap 通過 key 的 hashCode 經過擾動函數(所謂擾動函數指的就是 HashMap 的 hash 方法。使用 hash 方法也就是擾動函數是為了防止一些實現比較差的 hashCode() 方法 換句話說使用擾動函數之后可以減少碰撞。)處理過后得到 hash 值,然后通過 (n - 1) & hash 判斷當前元素存放的位置(這里的 n 指的是數組的長度),如果當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,如果相同的話,直接覆蓋,不相同就通過拉鏈法解決沖突。數據結構如下圖:

JDK1.8 以后的 HashMap 在解決哈希沖突時有了較大的變化,當鏈表長度大于閾值(默認為8)時,將鏈表轉化為紅黑樹,以減少搜索時間。數據結構圖如下:

HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。HashMap非線程安全,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。如果需要滿足線程安全,可以用 Collections的synchronizedMap方法使HashMap具有線程安全的能力,或者使用ConcurrentHashMap。

HashMap的初始化源碼:

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16static final int MAXIMUM_CAPACITY = 1 << 30;static final float DEFAULT_LOAD_FACTOR = 0.75f;transient Node<K,V>[] table; // 節點數組final float loadFactor; // 加載因子也稱填充因子int threshold; // 閥值,計算:threshold = loadFactor * capcity(節點數組容量)...public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}
}

從源碼中可以看到:

  1. DEFAULT_INITIAL_CAPACITY = 1 << 4;節點數組默認大小為16,這里使用位運算,相比乘法運算性能更快。
  2. Node<K,V>[] table,節點數組(也叫桶數組),就相當于是鏈表中的節點。需要注意的是Node類中的K是使用final關鍵字修飾的,也就說明了Map中的鍵(key)是不能修改的。
  3. threshold :閥值,計算方式為threshold = loadFactor * capcity(節點數組容量),當節點數組的使用容量超過閥值時就會以2的冪次方擴充節點數組。
  4. HashMap空參初始化的時候并沒有初始化節點數組,而只是設置了默認的加載因子,初始化大小會在第一次調用put方法后調用resize()方法進行初始化,其實數組的擴容也是調用這個方法進行的。

HashMap的put方法

HashMap的put方法的源碼有些多和難以理解(這里就不展示和解讀了,感興趣的可以自己去看源碼),我們看一張java8中put方法的處理邏輯圖:

這里我們重點說說resize()方法,在兩個地方會調用到resize方法:

  1. 當節點數組table為null時,會調用resize方發完成初始化
  2. 當節點數組table使用的容量大于閥值threshold時會調用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;}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 thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {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;...
}

從源碼中我們可以看到,初始化時table為空,緊接著就執行newCap = DEFAULT_INITIAL_CAPACITY;初始化新的容量newCap大小為默認容量大小16,最后再new一個容量為16的節點數組再將成員變量中的節點數組table的引用指向新的節點數組。所以說HashMap是在執行put()方法才初始化數組大小的。

我們看當oldCap>0,容量大于等于默認值且未超過最大值時,會通過newCap = oldCap << 1 和newThr = oldThr << 1; 將容量和加載因子都擴大一倍。也就是說,HashMap的擴容機制就是重新申請一個容量是當前的2倍的節點數組,然后將原先的記錄逐個重新映射到新的節點數組里面,然后將原先的節點數組里的節點逐個置為null使得引用失效。

剩余部分的代碼就是通過新插入的節點(每一個put(key,value)都會被封裝成一個節點)通過e.hash & (newCap - 1)找到節點數組的下標判斷是否有值…自己看圖。

為什么HashMap是不安全的?

其實HashMap之所以線程不安全就是出在了這個resize方法上,在resize操作的時候會造成線程不安全,如:put的時候導致的多線程數據不一致。

這個問題比較好想象,比如有兩個線程A和B,首先A希望插入一個key-value對到HashMap中,首先計算記錄所要落到的桶的索引坐標,然后獲取到該桶里面的鏈表頭結點,此時線程A的時間片用完了,而此時線程B被調度得以執行,和線程A一樣執行,只不過線程B成功將記錄插到了桶里面,假設線程A插入的記錄計算出來的桶索引和線程B要插入的記錄計算出來的桶索引是一樣的,那么當線程B成功插入之后,線程A再次被調度運行時,它依然持有過期的鏈表頭但是它對此一無所知,以至于它認為它應該這樣做,如此一來就覆蓋了線程B插入的記錄,這樣線程B插入的記錄就憑空消失了,造成了數據不一致的行為。

2.LinkedHashMap

LinkedHashMap是HashMap的一個子類,保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的,也可以在構造時帶參數,按照訪問次序排序。看名字也可以猜到,LinkedHashMap就是在HashMap的基礎上加了一個鏈表來保存記錄的插入順序。結構圖如下:

3.TreeMap

TreeMap實現SortedMap接口,能夠把它保存的記錄根據鍵排序,默認是按鍵值的升序排序,也可以指定排序的比較器,當用Iterator遍歷TreeMap時,得到的記錄是排過序的。如果使用排序的映射,建議使用TreeMap。在使用TreeMap時,key必須實現Comparable接口或者在構造TreeMap傳入自定義的Comparator,否則會在運行時拋出java.lang.ClassCastException類型的異常

這里順便提一下Comparable與Comparator的簡單區別:

  • comparable是java.lang包下的一個接口,使用compareTo(Object obj)方法來進行排序的。
  • comparator是java.util包下的一個接口,使用compare(Object obj1,Object obj2)方法來進行排序的。

線程安全類

1.HashTable

HashTable和HashMap的實現原理幾乎一樣,差別無非是1.HashTable不允許key和value為null;2.HashTable是線程安全的。但是HashTable線程安全的策略實現代價卻太大了,簡單粗暴,get/put所有相關操作都是synchronized的,這相當于給整個哈希表加了一把大鎖,多線程訪問時候,只要有一個線程訪問或操作該對象,那其他線程只能阻塞,相當于將所有的操作串行化,在競爭激烈的并發場景中性能就會非常差。Hashtable不建議在新代碼中使用,不需要線程安全的場合可以用HashMap替換,需要線程安全的場合可以用ConcurrentHashMap替換。數據結構如下圖:

2.ConcurrentHashMap

ConcurrentHashMap是Java并發包中提供的一個線程安全且高效的HashMap實現

1.分段鎖機制
概念由來:上面我們已經說了HashTable線程安全的策略實現代價太大了,它將get/put所有相關操作都是使用synchronized修飾的,這相當于給整個哈希表加了一把大鎖,多線程訪問時候,只要有一個線程訪問或操作該對象,那其他線程只能阻塞,相當于將所有的操作串行化,在競爭激烈的并發場景中性能就會非常差。所以在jdk1.5~1.7版本提出了分段機制概念。

原理:分段鎖對整個桶數組進行了分割分段(Segment),每一把鎖只鎖容器其中一部分數據,多線程訪問容器里不同數據段的數據,就不會存在鎖競爭,提高并發訪問率。結構圖如下:

2.jdk1.8版本(現在所用的實現方式)
在jdk1.8版本中,由于采用分段鎖時,最大并發數受限于segment個數,有很大的局限性,所以在jdk1.8版本中ConcurrentHashMap取消了Segment分段鎖,采用CAS和synchronized來保證并發安全。數據結構跟HashMap1.8的結構類似,數組+鏈表/紅黑二叉樹。Java 8在鏈表長度超過一定閾值(8)時將鏈表(尋址時間復雜度為O(N))轉換為紅黑樹(尋址時間復雜度為O(log(N)))synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不沖突,就不會產生并發,性能得到了很大的提升。

JDK1.8的ConcurrentHashMap(TreeBin: 紅黑二叉樹節點 Node: 鏈表節點):

參考

https://www.jianshu.com/p/e2f75c8cce01
https://snailclimb.top/JavaGuide/#/java/Multithread

總結

以上是生活随笔為你收集整理的Map再整理,从底层源码探究HashMap的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。