你知道的越多,不知道的就越多,業餘的像一棵小草!
你來,咱們一塊兒精進!你不來,我和你的競爭對手一塊兒精進!
web
編輯:業餘草
推薦:https://www.xttblog.com/?p=5167
前言面試
-
對key對象的hashcode進行擾動 -
經過取模求得數組下標
static final int hash(Object key) {
int h;
// 獲取到key的hashcode,在高低位異或運算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
算法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
// 與數組長度-1進行位與運算,獲得下標
if ((p = tab[i = (n - 1) & hash]) == null)
...
}
api
-
初始化時指定的長度 -
擴容時的長度增量
public HashMap(int initialCapacity, float loadFactor) {
...
this.loadFactor = loadFactor;
// 這裏調用了tableSizeFor方法
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;
}
數組
00100 --高位1以後全變1--> 00111 --加1---> 01000
安全
final Node<K,V>[] resize() {
...
if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 設置爲原來的兩倍
newThr = oldThr << 1;
...
}
微信
-
HashMap經過高16位與低16位進行異或運算來讓高位參與散列,提升散列效果; -
HashMap控制數組的長度爲2的整數次冪來簡化取模運算,提升性能; -
HashMap經過控制初始化的數組長度爲2的整數次冪、擴容爲原來的2倍來控制數組長度必定爲2的整數次冪。
-
當鏈表的長度>=8且數組長度>=64時,會把鏈表轉化成紅黑樹。 -
當鏈表長度>=8,但數組長度<64時,會優先進行擴容,而不是轉化成紅黑樹。 -
當紅黑樹節點數<=6,自動轉化成鏈表。
-
HashMap採用鏈地址法,當發生衝突時會轉化爲鏈表,當鏈表過長會轉化爲紅黑樹提升效率。 -
HashMap對紅黑樹進行了限制,讓紅黑樹只有在極少數極端狀況下進行抗壓。
-
裝載因子決定了HashMap擴容的閾值,須要權衡時間與空間,通常狀況下保持0.75不做改動; -
HashMap擴容機制結合了數組長度爲2的整數次冪的特色,以一種更高的效率完成數據遷移,同時避免頭插法形成鏈表環。
-
採用Hashtable -
調用Collections.synchronizeMap()方法來讓HashMap具備多線程能力 -
採用ConcurrentHashMap
// Hashtable
public synchronized V get(Object key) {...}
public synchronized V put(K key, V value) {...}
public synchronized V remove(Object key) {...}
public synchronized V replace(K key, V value) {...}
...
數據結構
final Object mutex; // Object on which to synchronize
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
// 默認爲本對象
mutex = this;
}
SynchronizedMap(Map<K,V> m, Object mutex) {
this.m = m;
this.mutex = mutex;
}
多線程
-
鎖是很是重量級的,會嚴重影響性能。 -
同一時間只能有一個線程進行讀寫,限制了併發效率。
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("abc","123");
Thread1:
if (map.containsKey("abc")){
String s = map.get("abc");
}
Thread2:
map.remove("abc");
併發
final Node<K,V> nextNode() {
...
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
...
}
-
HashMap並不能保證線程安全,在多線程併發訪問下會出現意想不到的問題,如數據丟失等 -
HashMap1.8採用尾插法進行擴容,防止出現鏈表環致使的死循環問題 -
解決併發問題的的方案有Hashtable、Collections.synchronizeMap()、ConcurrentHashMap。其中最佳解決方案是ConcurrentHashMap -
上述解決方案並不能徹底保證線程安全 -
快速失敗是HashMap迭代機制中的一種併發安全保證
關鍵變量的理解
// 存放數據的數組
transient Node<K,V>[] table;
// 存儲的鍵值對數目
transient int size;
// HashMap結構修改的次數,主要用於判斷fast-fail
transient int modCount;
// 最大限度存儲鍵值對的數目(threshodl=table.length*loadFactor),也稱爲閾值
int threshold;
// 裝載因子,表示可最大容納數據數量的比例
final float loadFactor;
// 靜態內部類,HashMap存儲的節點類型;可存儲鍵值對,自己是個鏈表結構。
static class Node<K,V> implements Map.Entry<K,V> {...}
擴容
final Node<K,V>[] resize() {
// 變量分別是原數組、原數組大小、原閾值;新數組大小、新閾值
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 若是原數組長度大於0
if (oldCap > 0) {
// 若是已經超過了設置的最大長度(1<<30,也就是最大整型正數)
if (oldCap >= MAXIMUM_CAPACITY) {
// 直接把閾值設置爲最大正數
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 設置爲原來的兩倍
newThr = oldThr << 1;
}
// 原數組長度爲0,但最大限度不是0,把長度設置爲閾值
// 對應的狀況就是新建HashMap的時候指定了數組長度
else if (oldThr > 0)
newCap = oldThr;
// 第一次初始化,默認16和0.75
// 對應使用默認構造器新建HashMap對象
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 若是原數組長度小於16或者翻倍以後超過了最大限制長度,則從新計算閾值
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;
if (oldTab != null) {
// 循環遍歷原數組,並給每一個節點計算新的位置
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)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 新的位置只有兩種可能:原位置,原位置+老數組長度
// 把原鏈表拆成兩個鏈表,而後再分別插入到新數組的兩個位置上
// 不用屢次調用put方法
else {
// 分別是原位置不變的鏈表和原位置+原數組長度位置的鏈表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍歷老鏈表,判斷新增斷定位是1or0進行分類
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 最後賦值給新的數組
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新數組
return newTab;
}
添加數值
public V put(K key, V value) {
// 獲取hash值,再調用putVal方法插入數據
return putVal(hash(key), key, value, false, true);
}
// onlyIfAbsent表示是否覆蓋舊值,true表示不覆蓋,false表示覆蓋,默認爲false
// evict和LinkHashMap的回調方法有關,不在本文討論範圍
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab是HashMap內部數組,n是數組的長度,i是要插入的下標,p是該下標對應的節點
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判斷數組是不是null或者是不是空,如果,則調用resize()方法進行擴容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 使用位與運算代替取模獲得下標
// 判斷當前下標是不是null,如果則建立節點直接插入,若不是,進入下面else邏輯
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// e表示和當前key相同的節點,若不存在該節點則爲null
// k是當前數組下標節點的key
Node<K,V> e; K k;
// 判斷當前節點與要插入的key是否相同,是則表示找到了已經存在的key
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判斷該節點是不是樹節點,若是是調用紅黑樹的方法進行插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 最後一種狀況是直接鏈表插入
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 長度大於等於8時轉化爲紅黑樹
// 注意,treeifyBin方法中會進行數組長度判斷,
// 若小於64,則優先進行數組擴容而不是轉化爲樹
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 找到相同的直接跳出循環
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 若是找到相同的key節點,則判斷onlyIfAbsent和舊值是否爲null
// 執行更新或者不操做,最後返回舊值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 若是不是更新舊值,說明HashMap中鍵值對數量發生變化
// modCount數值+1表示結構改變
++modCount;
// 判斷長度是否達到最大限度,若是是則進行擴容
if (++size > threshold)
resize();
// 最後返回null(afterNodeInsertion是LinkHashMap的回調)
afterNodeInsertion(evict);
return null;
}
-
整體上分爲兩種狀況:找到相同的key和找不到相同的key。找了須要判斷是否更新並返回舊value,沒找到須要插入新的Node、更新節點數並判斷是否須要擴容。 -
查找分爲三種狀況:數組、鏈表、紅黑樹。數組下標i位置不爲空且不等於key,那麼就須要判斷是否樹節點仍是鏈表節點並進行查找。 -
鏈表到達必定長度後須要擴展爲紅黑樹,當且僅當鏈表長度>=8且數組長度>=64。
爲何jdk1.7之前控制數組的長度爲素數,而jdk1.8以後卻採用的是2的整數次冪?
爲何插入HashMap的數據須要實現hashcode和equals方法?對這兩個方法有什麼要求?
本文分享自微信公衆號 - 業餘草(yyucao)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。