什麼是邊緣計算?啟揚智慧系列智慧邊緣計算閘道器
Java原始碼解析之HashMap
一、HashMap原始碼解析
1、HashMap的資料結構
- jdk7以前:陣列+連結串列
- jdk8以後:陣列+連結串列+紅黑樹
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; ... ... } transient Node<K,V>[] table; }
以上程式碼是jdk中HashMap中的部分程式碼
HashMap類中定義了一個名為Node的靜態內部類,它實現了Map.Entry介面,這個內部類例項化後就是一個Entry的實現類物件,每個
Entry物件中有指向下一個元素的Node<K,V> next屬性,這就是所謂的連結串列的實現方式(通過地址指引)
HashMap類中還定義了一個Node<K,V>[] table的陣列,用於儲存一個個的Node物件
從這裡可以看出HashMap中儲存的起始是一個個的Entry物件
小知識點
序列化:一個類實現了Serializable,就相當於告訴JVM這個類是可以被序列化的,就是可以把物件通過流寫出
transient關鍵字:在一個可序列化的類中,transient關鍵字修飾的成員表示該成員不參與序列化,也就是不能寫出儲存,相當 於是一個臨時資料
反序列化:就是把序列化寫出的物件讀到程式記憶體中
下面是Node內部類的具體原始碼
static class Node<K,V> implements Map.Entry<K,V> { final int 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; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } 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; } }
2、HashMap的構造方法
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient int modCount; //記錄這個HashMap結構被修改的次數
transient int size; //這個HashMap中包含鍵值對的數量
int threshold; //下一次擴容的條件數值,當陣列內容達到這個值時進行擴容
}
HashMap類存在這三種常量屬性,和雜湊表的效能有關,分別是
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
:定義了Node<K,V>[] table
陣列的預設大小
static final int MAXIMUM_CAPACITY = 1 << 30
:定義了Node<K,V>[] table
陣列的最大長度
static final float DEFAULT_LOAD_FACTOR = 0.75f
:定義了雜湊表的載入因子
1 << 4 << 是移位操作,是基於二進位制的操作
1的二進位制 0000 0001 就是十進位制的1
1 << 4 後 0001 0000 就是十進位制的16
載入因子:載入因子就是判斷陣列什麼時候進行擴容,當陣列內元素個數滿足 原陣列長度*載入因子 個時進行擴容
-
空構造public HashMap()
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
呼叫空建構函式時,生成物件的欄位屬性使用的都是預設值
陣列預設長度16bit、載入因子0.75
-
包含陣列長度和載入因子的建構函式public HashMap(int initialCapacity, float loadFactor)
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); }
這個構造方法中,傳了一個數組的長度和載入因子的陣列作為引數,函式內就是判斷這兩個值是否符合條件,符合的話就為物件的loadFactor屬性賦值,並使用 this.threshold = tableSizeFor(initialCapacity);為threshold 賦值;否則報錯
tableSizeFor這個函式返回一個大於指定值得最小2次冪
使用 this.threshold = tableSizeFor(initialCapacity)為threshold 賦值???
threshold的值不應該是 this.threshold = tableSizeFor(initialCapacity)*loadFactor嗎?
在HashMap的建構函式,並沒有為Node<K,V>[] table進行初始化,而是把初始化的過程放在了put方法裡
-
包含陣列長度的建構函式public HashMap(int initialCapacity)
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
這個就是呼叫public HashMap(int initialCapacity, float loadFactor)構造,把載入因子為預設值
3、HashMap的存值(put方法)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
呼叫put方法時,put方法會把key的hash值計算出來,然後呼叫putVal方法,返回值為Value的值
-
1.putVal方法會先判斷陣列是否需要擴容
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
如果陣列為空或者長度為零,則呼叫resize方法擴容陣列,這裡才對陣列進行初始化操作
-
2.然後計算出元素的索引,判斷陣列上該位置是否已經存過元素
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
如果該位置為空,則直接存入該位置
(n-1) & hash 是求該hash值在陣列中的索引的操作
n ---> 陣列長度
例如:陣列長度為16 hash值為一個隨機值
n 0001 0000 16
n-1 0000 1111 15
hash 1011 0011 隨機
& 0000 0011 &後的結果 3
(n-1) & hash相當於拿出一個隨機值的後幾位
-
3.如果陣列當前索引處已經存有元素了,把陣列當前索引存的物件稱為 p
-
判斷這個元素的key值是否和p的key值相同
else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
如果相同則取出p物件,否則
-
判斷p是否為一個樹節點
else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
如果p是一個樹節點,呼叫putTreeVal方法
-
遍歷陣列這個索引下的節點
-
如果存在和要存元素key值相同的元素
如果存在,則把這個元素記錄到e上,用於後續判斷
-
如果不存在,則把新元素插入到連結串列的尾部
else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } }
-
-
如果執行完上面的程式碼取出的元素不為空,則說明存在和要存物件key值相同的元素
if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
用新的value值替換舊的value值,返回舊的value值
-
最後說明插入成功,將陣列的元素數量加一,判斷是否需要擴容
++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
-
這就是put方法的全過程,此外,put方法結尾還提供了一個afterNodeAccess(e)方法,這是一個空方法,沒有任何實現,它允許我們插入元素後進行一些操作
4、HashMap的取值(get方法)
get方法返回一個value值,原始碼如下
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
get方法通過呼叫getNode方法來找到相應的鍵值對物件,下面是getNode方法的原始碼
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
5、HashMaps陣列擴容(resize方法)
因為在HashMap的構造方法裡,並沒有對陣列進行初始化,而是在put方法裡呼叫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;
先在resize方法裡定義了一些變數來表示陣列的一些資訊
-
如果陣列已經初始化過,分為兩種情況
-
陣列長度已經大於設定的最大容量了
這時,把擴容條件閾值設為整數的最大值,這樣在put方法插入元素後的判斷
if (++size > threshold)
就始終為false(因為size是int型別,不會大於Integer.MAX_VALUE),這樣陣列就不會再進行擴容 -
陣列長度介於預設值和最大值之間
這時,陣列將進行正常的擴容操作,陣列長度和擴容條件閾值都變為原來的兩倍
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 }
-
-
如果陣列未初始化,且條件閾值大於零
什麼時候才會發生這種情況呢?
當我們呼叫HashMap的非空構造方法時,在構造方法裡並沒有對陣列進行初始化,而是計算出來擴容閾值threshold的值,這樣就會出現陣列長度為零,擴容條件閾值存在的情況
else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr;
這種情況下,直接把擴容閾值作為陣列的長度進行初始化
-
如果陣列未初始化且條件閾值不存在
不存在的意思是沒有賦過值,這時threshold的值是預設值0
這種情況最常見,一般是呼叫了HashMap的空構造
else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); }
這種情況下,陣列長度和條件閾值都用預設值即可、
-
if (newThr == 0)?為什麼還要有這個判斷?
在執行
else if (oldThr > 0) newCap = oldThr;
這句後,也就是當呼叫HashMap的有參構造時,這裡並沒有為newThr賦值,因此newThr為預設值0if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); }
這個判斷就是為newThr賦值
然後把擴容後的資訊更新到物件中
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
-
擴容完畢,將原來的資料轉移到新陣列中
對這段程式碼先不做解釋,諒解
二、HashMap執行緒不安全的原因
HashMap在java7以前,存在的執行緒安全問題有死迴圈、資料丟失、資料覆蓋;在java8以後,HashMap存在的執行緒安全問題是資料覆蓋
-
多執行緒問題1-->資料覆蓋
前提:多個執行緒進行put存值時,多個執行緒的要存的索引位置相同,並且滿足儲存條件(不存在key值相同)
執行緒一判斷完滿足儲存條件後掛起 -----> 執行緒二啟動,並在線上程一要存的位置存值 ---> 執行緒一繼續執行,在相應位置存值
這時就把執行緒二所存的值覆蓋掉了
此外,在
if (++size > threshold)
這句程式碼裡,++size明顯是一個執行緒不安全的操作 -
多執行緒問題2-->死迴圈
以下是jdk7以前的HashMap陣列擴容後資料遷移的原始碼
//資料遷移的方法,頭插法新增元素 void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //for迴圈中的程式碼,逐個遍歷連結串列,重新計算索引位置,將老陣列資料複製到新陣列中去(陣列不儲存實際資料,所以僅僅是拷貝引用 //而已) for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); //將當前entry的next鏈指向新的索引位置,newTable[i]有可能為空,有可能也是個entry鏈,如果是entry鏈,直接在鏈 //表頭部插入。 //以下三行是執行緒不安全的關鍵 e.next = newTable[i]; newTable[i] = e; e = next; } } }
會發生執行緒安全問題的程式碼主要是
e.next = newTable[i]; newTable[i] = e;
這兩行.
-
多執行緒問題2-->資料丟失
後面會更新,敬請見諒!