摘要:線程安全關(guān)于線程安全,我們想要知道的是在什么情況下會發(fā)生線程不安全的情況實際上,在上文分析方法時,當(dāng)?shù)娜萘砍^了時,便執(zhí)行操作,就存在線程不安全的問題。實現(xiàn)了所謂的線程安全,在很多方法上都加上了。
HashMap簡介
本文針對HashMap的源碼分析基于JDK 7,JDK 8在HashMap的實現(xiàn)上有著較大幅度的改進和優(yōu)化,這部分優(yōu)化我將另起一篇來闡述。另外,本文僅分析HashMap眾多方法中最常用的方法,其余方法有需要時再研究 。
HashMap的繼承關(guān)系如下。
public class HashMapextends AbstractMap implements Map , Cloneable, Serializable
HashMap繼承自AbstractMap,同時實現(xiàn)了Map、Cloneable和Serializable接口。因此,HashMap可以被克隆,并支持序列化。另外,HashMap是一個非線程安全的,因此適合運用在單線程環(huán)境下。如果是在多線程環(huán)境,可以通過Collections的靜態(tài)方法synchronizedMap獲得線程安全的HashMap,如下代碼所示。
Map存儲結(jié)構(gòu)map = Collections.synchronizedMap(new HashMap ());
針對每個鍵值對,HashMap使用內(nèi)部類Entry來存儲,Entry核心代碼如下。
static class Entryimplements Map.Entry { final K key; V value; Entry next; final int hash; Entry(int h, K k, V v, Entry n) { value = v; next = n; key = k; hash = h; } }
從整體上看,HashMap底層的存儲結(jié)構(gòu)是基于數(shù)組和鏈表實現(xiàn)的。對于每一個要存入HashMap的鍵值對(Key-Value Pair),通過計算Key的hash值來決定存入哪個數(shù)組單元(bucket),為了處理hash沖突,每個數(shù)組單元實際上是一條Entry單鏈表的頭結(jié)點,其后引申出一條單鏈表。HashMap的存儲結(jié)構(gòu)如下圖所示。
關(guān)鍵屬性HashMap定義了幾個關(guān)鍵屬性,對應(yīng)的源碼如下。
static final int DEFAULT_INITIAL_CAPACITY = 16; static final int MAXIMUM_CAPACITY = 1 << 30; static final float DEFAULT_LOAD_FACTOR = 0.75f; transient Entry[] table; transient int size; int threshold; final float loadFactor;
DEFAULT_INITIAL_CAPACITY代表HashMap槽(bucket)的默認容量,且該容量必須為2的冪,具體原因會在下文解釋。
MAXIMUM_CAPACITY代表HashMap槽(bucket)的最大容量,如果傳入的容量大于1 << 30,那么實際容量會被MAXIMUM_CAPACITY替換。
DEFAULT_LOAD_FACTOR是默認的加載因子,用于計算HashMap擴容的threshold,當(dāng)HashMap的實際元素容量達到總?cè)萘康?b>threshold時,對HashMap進行擴容。
table是存儲Entry的數(shù)組,每個Entry是一條單鏈表的頭結(jié)點。
size代表HashMap鍵值對的數(shù)量。
threshold是HashMap決定是否執(zhí)行執(zhí)行擴容操作的閾值,threshold = capacity * load factor。
loadFactor表示HashMap實際加載因子,通過構(gòu)造方法傳入。若未指定,loadFactor等于DEFAULT_LOAD_FACTOR。
需要進一步解釋的是loadFactor屬性,loadFactor描述了HashMap發(fā)生擴容時的填充程度。如果loadFactor設(shè)置過大,意味著在HashMap擴容前發(fā)生hash沖突的機會越大,因此單鏈表的長度也就會越長,那么在執(zhí)行查找操作時,會由于單鏈表長度過長導(dǎo)致查找的效率降低。如果loadFactor設(shè)置過小,那么HashMap的空間利用率會降低,導(dǎo)致HashMap在很多空間都沒有被利用的情況下便開始擴容。
構(gòu)造方法HashMap定義了四個構(gòu)造方法,源碼如下。
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); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); // 在源碼中,init方法體不執(zhí)行任何操作。 } public HashMap(Map extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); putAllForCreate(m); }
當(dāng)調(diào)用HashMap默認構(gòu)造方法時,HashMap對象的屬性均會被設(shè)置為默認值,包括設(shè)置加載因子(DEFAULT_LOAD_FACTOR)、擴容閾值(threshold)和table的初始大小。
如果在創(chuàng)建HashMap對象時指定了bucket容量initialCapacity,通過源碼我們可以看出在初始化對象時不一定會直接使用initialCapacity,而是選取滿足小于等于initialCapacity前提條件下最大的且是2的冪的一個值作為實際bucket的大小。
如果向構(gòu)造方法傳遞的參數(shù)是一個Map對象m,那么putAllForCreate方法會重新散列m中的每個元素,將它們存入相應(yīng)的bucket中。putAllForCreate方法及其調(diào)用的相關(guān)方法如下。
private void putForCreate(K key, V value) { int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entrye = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { e.value = value; return; } } createEntry(hash, key, value, i); } private void putAllForCreate(Map extends K, ? extends V> m) { for (Map.Entry extends K, ? extends V> e : m.entrySet()) putForCreate(e.getKey(), e.getValue()); } static int indexFor(int h, int length) { return h & (length-1); } void createEntry(int hash, K key, V value, int bucketIndex) { Entry e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
putAllForCreate方法遍歷每一個鍵值對e,通過putForCreat方法將e散列到對應(yīng)的bucket中。putForCreate方法調(diào)用indexFor來確定鍵值對散列的bucket的位置。indexFor通過h & (length-1)返回bucket的位置,接著遍歷對應(yīng)的單鏈表來決定是更新操作還是插入操作。
我們需要關(guān)注的地方是indexFor為什么通過計算h & (length-1)來獲得bucket的位置,而不是通過計算h % length?
實際上,在HashMap中,h & (length-1) == h % length,但是需要一個前提:length必須滿足是2的冪。這也正是在解釋DEFAULT_INITIAL_CAPACITY和HashMap構(gòu)造方法時強調(diào)的HashMap的bucket容量必須是2的冪。當(dāng)length是2的冪,那么length的二進制數(shù)可以表示為1000...000,因此length - 1的二進制數(shù)為0111...111,當(dāng)h與length - 1位與時,除了h的最高位的被修改為0,其余位均保持不變,這也正是實現(xiàn)了h % length的效果。只是相比于h % length,h & (length-1)的效率會更高。
HashMap的bucket容量必須為2的冪的另一個重要原因是一旦滿足此條件,那么length即為偶數(shù),length - 1便為奇數(shù),所以length - 1的最后一位必為1。因此,h & (length - 1)得到的值既可能是奇數(shù),也可能是偶數(shù),這確保了散列的均勻性。如果length - 1是偶數(shù),那么h & (length - 1)得到的值必為偶數(shù),那么HashMap的空間便浪費了一半。
存取方法我們分析HashMap使用頻率最高的兩個方法get方法和put方法,源碼如下。
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entrye = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; } public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
從HashMap獲取get元素時,先計算Key的hash值,定位到數(shù)組中對應(yīng)的bucket,然后開始遍歷Entry單鏈表,直到找到需要的元素,否則返回null。
當(dāng)我們向HashMap中put新的鍵值對時,HashMap首先檢查Key是否等于null,若為null,則執(zhí)行putForNullKey方法,putForNullKey方法對應(yīng)的源碼如下。
private V putForNullKey(V value) { for (Entrye = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); // 不做任何操作 return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
如果Key等于null,那么就將該鍵值對添加到table[0]的位置,同時,遍歷table[0]處的單鏈表并將鏈表中所有節(jié)點的值都覆蓋為新傳遞進來的鍵值對的值。因此,該位置永遠只有一個值。
如果Key不等于null,那么通過indexFor定位到bucket,然后遍歷單鏈表,如果存在Key相等的鍵值對,就用新值覆蓋舊值,并返回舊值。如果在單鏈表中沒有找到對應(yīng)的Key,那么調(diào)用addEntry方法創(chuàng)建新的Entry節(jié)點至單鏈表(作為頭節(jié)點)。addEntry及關(guān)聯(lián)方法源碼如下。
void addEntry(int hash, K key, V value, int bucketIndex) { Entrye = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); } void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); } void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry e = src[j]; if (e != null) { src[j] = null; do { Entry next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
當(dāng)addEntry把新增鍵值對插入單鏈表后,會判斷是否需要擴容,即判斷當(dāng)前HashMap的元素的個數(shù)是否大于threshold。若需要擴容,那么調(diào)用resize方法進行2倍擴容。resize方法會在內(nèi)部調(diào)用transfer方法,transfer方法遍歷舊數(shù)組及單鏈表,并將每個鍵值對重新散列,可以意識到,這整個rehash的開銷相當(dāng)大。
線程安全關(guān)于線程安全,我們想要知道的是HashMap在什么情況下會發(fā)生線程不安全的情況?實際上,在上文分析put方法時,當(dāng)HashMap的容量超過了threshold時,便執(zhí)行resize操作,resize就存在線程不安全的問題。
關(guān)于resize哪兒不安全,我推薦左耳朵耗子寫的疫苗:Java HashMap的死循環(huán),這篇文章圖文并茂的解釋了在rehash過程中出現(xiàn)線程不安全問題的根源。
HashMap VS HashTableHashTable和HashMap底層采用相同的存儲結(jié)構(gòu),在很多方法的實現(xiàn)上二者的思路基本一致。最主要的區(qū)別主要有兩點。
HashTable實現(xiàn)了所謂的線程安全,在HashTable很多方法上都加上了synchronized。
在HashMap的分析中,我們發(fā)現(xiàn)當(dāng)我們新增鍵值對時,HashMap是允許Key和Value均為null。但是HashTable不允許Key或Value為null,關(guān)于這一點我們可以通過查看HashTable源碼得知。
public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { // 若value為空則拋出NullPointerException。 throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry,?> tab[] = table; int hash = key.hashCode(); // 若key為空則拋出NullPointerException。 int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entryentry = (Entry )tab[index]; for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } addEntry(hash, key, value, index); return null; }
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/66322.html
摘要:當(dāng)一個值中要存儲到的時候會根據(jù)的值來計算出他的,通過哈希來確認到數(shù)組的位置,如果發(fā)生哈希碰撞就以鏈表的形式存儲在源碼分析中解釋過,但是這樣如果鏈表過長來的話,會把這個鏈表轉(zhuǎn)換成紅黑樹來存儲。 正文開始 注:JDK版本為1.8 HashMap1.8和1.8之前的源碼差別很大 目錄 簡介 數(shù)據(jù)結(jié)構(gòu) 類結(jié)構(gòu) 屬性 構(gòu)造方法 增加 刪除 修改 總結(jié) 1.HashMap簡介 H...
摘要:簡介繼續(xù)分析源碼,上一篇文章把的分析完畢。本文開始分析簡單的介紹一下。存儲的元素是無序的并且允許使用空的元素。 1.簡介 繼續(xù)分析源碼,上一篇文章把HashMap的分析完畢。本文開始分析HashSet簡單的介紹一下。 HashSet是一個無重復(fù)元素集合,內(nèi)部使用HashMap實現(xiàn),所以HashMap的特征耶繼承了下來。存儲的元素是無序的并且HashSet允許使用空的元素。 HashSe...
摘要:則使用了拉鏈式的散列算法,并在中引入了紅黑樹優(yōu)化過長的鏈表。如果大家對紅黑樹感興趣,可以閱讀我的另一篇文章紅黑樹詳細分析。構(gòu)造方法構(gòu)造方法分析的構(gòu)造方法不多,只有四個。 1.概述 本篇文章我們來聊聊大家日常開發(fā)中常用的一個集合類 - HashMap。HashMap 最早出現(xiàn)在 JDK 1.2中,底層基于散列算法實現(xiàn)。HashMap 允許 null 鍵和 null 值,在計算哈鍵的哈希值...
閱讀 3580·2023-04-25 20:41
閱讀 2674·2023-04-25 16:40
閱讀 1445·2021-09-23 11:44
閱讀 1263·2021-09-10 10:51
閱讀 1692·2021-09-07 09:59
閱讀 1679·2019-12-27 12:08
閱讀 567·2019-08-30 15:44
閱讀 3344·2019-08-30 11:08