今天先為JAVA集合系列原始碼開一個頭,也嘗試著用不同的方式,不同的角度去閱讀原始碼,去學習原始碼中的一些思想。HashMap作為最常使用的集合之一;JDK1.7之前,有很大的爭議,一方面是資料量變大之後的查詢效率問題,還有就是執行緒安全問題。本文將從JDK1.7和1.8兩個不同版本的原始碼入手,去探尋一下HashMap是如何被優化的,以及執行緒安全問題的出現情況。
jdk1.7
HashMap在1.7中和1.6的主要區別:
- 加入了jdk.map.althashing.threshold這個jdk的引數用來控制是否在擴容時使用String型別的新hash演算法。
- 把1.6的構造方法中對錶的初始化挪到了put方法中。
- 1.6中的tranfer方法對舊錶的節點進行置null操作(存在多執行緒問題),1.7中去掉了。
定義
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
複製程式碼
HashMap繼承自AbstractMap,實現了Map,Cloneable,和Serializable。既然實現了Serializable介面,也就是說可以實現序列化,在下面的成員變數介紹中可以看到,table[]使用了transient來修飾的,這個對於大多數集合框架中的類來說都有這種機制。查閱了相關資料和結合網上各路大神的解釋,這裡總結一下:
-
減少不必要的空值序列化
table 以及 elementData中儲存的資料的數量通常情況下是要小於陣列長度的(擴容機制),這個在資料越來越多的情況下更為明顯(資料變多,伴隨著衝突概率變大,同時也伴隨著擴容)。如果使用預設的序列化,那些沒有資料的位置也會被儲存,就會產生很多不必要的浪費。
-
不同虛擬機器的相容問題
由於不同的虛擬機器對於相同hashCode產生的Code值可能是不一樣的,如果使用預設的序列化,那麼反序列化後,元素的位置和之前的是保持一致的,可是由於hashCode的值不一樣了,那麼定位到的桶的下標就會不同,這很明顯不是我們想看到的
所在HashMap的序列化 並沒有使用預設的序列化方法,而採用自定義的序列化方法,通過重寫writeObject方法來完成。
成員變數
- 預設初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
複製程式碼
- 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
複製程式碼
- 預設負載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
複製程式碼
- 空表例項
static final Entry<?,?>[] EMPTY_TABLE = {};
複製程式碼
- table,一個Entry陣列
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
複製程式碼
- size,map中key-value的總數
transient int size;
複製程式碼
- 閾值,當map中key-value的總數達到這個值時,進行擴容
int threshold;
複製程式碼
- 負載因子
final float loadFactor;
複製程式碼
- 被修改的次數(fial-fast機制)
transient int modCount;
複製程式碼
- alternative hashing(如果使用了String型別的一種新hash演算法)的預設閾值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
複製程式碼
- 控制是否重新hash,預設為0,後面會詳細說明
transient int hashSeed = 0;
複製程式碼
- 內部類,VM啟動之後初始化
jdk.map.althashing.threshold
複製程式碼
JDK中的引數,預設-1,如果設定為1,則強制使用String型別的新hash演算法
private static class Holder {
/**
* Table capacity above which to switch to use alternative hashing.
*/
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
// disable alternative hashing if -1
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
ALTERNATIVE_HASHING_THRESHOLD = threshold;
}
}
複製程式碼
內部結構
從上圖可以看出,HashMap是基於陣列+連結串列的方式實現的。來看Entry這個內部類:- Entry,4個屬性(key,value,next節點,hash值)
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
複製程式碼
建構函式
- 無參建構函式,預設初始容量16,負載因子0.75
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
複製程式碼
- 一個引數建構函式,預設負載因子0.75
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;
threshold = initialCapacity;
init();
}
複製程式碼
- init方法,模板方法,如果有子類需要擴充套件可以自行實現
void init() {
}
複製程式碼
主要方法
- hash方法
final int hash(Object k) {
int h = hashSeed;
//預設0,如果不是0,並且key是String型別,才使用新的hash演算法(避免碰
//撞的優化?)
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
//把高位的值移到低位參與運算,使高位值的變化會影響到hash結果
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
複製程式碼
- 根據hash值確定在table中的位置,length為2的倍數 HashMap的擴容是基於2的倍數來擴容的,從這裡可以看出,對於indexFor方法而言,其具體實現就是通過一個計算出來的code值和陣列長度-1做位運算,那麼對於2^N來說,長度減一轉換成二進位制之後就是全一(長度16,len-1=15,二進位制就是1111),所以這種設定的好處就是說,對於計算出來的code值得每一位都會影響到我們索引位置的確定,其目的就是為了能讓資料更好的雜湊到不同的桶中。
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
複製程式碼
put方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
//如果表沒有初始化,則以閾值threshold的容量初始化表
inflateTable(threshold);
}
if (key == null)
//如果key值為null,呼叫putForNullKey方法,所以hashmap可以插入key和value為null的值
return putForNullKey(value);
//計算key的hash值
int hash = hash(key);
//計算hash值對應表的位置,即表下標
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//如果hash值相等並且(key值相等或者key的equals方法相等),
//則覆蓋,返回舊的value
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//修改字數+1
modCount++;
//如果沒找到key沒找到,則插入
addEntry(hash, key, value, i);
return null;
}
複製程式碼
- 初始化表方法,表容量必須是2的倍數(roundUpToPowerOf2)
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
複製程式碼
- 獲取大於或等於給定值的最小的2的倍數
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
複製程式碼
- highestOneBit:返回小於給定值的最大的2的倍數
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1); //其餘位不管,把最高位的1覆蓋到第二位,使前2位都是1
i |= (i >> 2); //同樣的,把第3、4位置1,使前4位都是1
i |= (i >> 4); //...
i |= (i >> 8); //...
i |= (i >> 16); //最高位以及低位都是1
return i - (i >>> 1); //返回最高位為1,其餘位全為0的值
}
複製程式碼
- initHashSeedAsNeeded方法控制transfer擴容時是否重新hash
final boolean initHashSeedAsNeeded(int capacity) {
//hashSeed預設0,currentAltHashing為false
boolean currentAltHashing = hashSeed != 0;
//參照上面的Holder類的靜態塊,jdk.map.althashing.threshold預設-1,Holder.ALTERNATIVE_HASHING_THRESHOLD為Integer.MAX_VALUE,如果jdk.map.althashing.threshold設定了其他非負數,可以改變Holder.ALTERNATIVE_HASHING_THRESHOLD的值,如果不超過Integer.MAX_VALUE,則useAltHashing為true
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) { //改變hashSeed的值,使hashSeed!=0,rehash時String型別會使用新hash演算法
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
複製程式碼
- HashMap把key為null的key-value鍵值對放入table[0]中
private V putForNullKey(V value) {
for (Entry<K,V> e = 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-value鍵值對
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); //如果鍵值對數量達到了閾值,則擴容
hash = (null != key) ? hash(key) : 0; //null的hash值為0
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
複製程式碼
-
頭插法,即把新的Entry插入到table[bucketIndex]的連結串列頭位置
關於頭插法的解釋:一般情況下會預設後插入的資料被查詢的頻次會高一點。
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
複製程式碼
get方法
public V get(Object key) {
if (key == null)
return getForNullKey(); //如果key為null,直接去table[0]中找
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
複製程式碼
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
複製程式碼
- getEntry方法比較簡單,先找hash值在表中的位置,再迴圈連結串列查詢Entry,如果存在,返回Entry,否則返回null
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
複製程式碼
- remove方法
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
複製程式碼
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i]; //前一個節點
Entry<K,V> e = prev; //當前節點
while (e != null) {
Entry<K,V> next = e.next; //下一個節點
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e) //如果相等,說明需要刪除的是頭節點,頭節點直接等於next
table[i] = next;
else
prev.next = next; //如果不是頭節點,前一個的next等於下一個節點,刪除當前節點
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
複製程式碼
resize方法,擴容(重點)
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) { //如果容量已經達到MAXIMUM_CAPACITY,不擴容
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
//initHashSeedAsNeeded方法決定是否重新計算String型別的hash值
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
複製程式碼
- transfer方法,把舊錶的所有節點轉移到新表中
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
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);
}
/**
*重新計算hash值在新表中的位置(舊錶中一條連結串列中的資料
*最多會分成兩條存在新表中,即oldTable[index]中的節點會存到
*newTable[index]和newTable[index+oldTable.length]中)
*/
int i = indexFor(e.hash, newCapacity);
//頭插法插入到新表中
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
複製程式碼
jdk1.8
1.8的HashMap相比於1.7有了很多變化
- Entry結構變成了Node結構,hash變數加上了final宣告,即不可以進行rehash了
- 插入節點的方式從頭插法變成了尾插法
- 引入了紅黑樹
- tableSizeFor方法、hash演算法等等
定義
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;
複製程式碼
- 預設負載因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
複製程式碼
- 連結串列轉紅黑樹的閾值 8
static final int TREEIFY_THRESHOLD = 8;
複製程式碼
- 紅黑樹轉連結串列的閾值 6
static final int UNTREEIFY_THRESHOLD = 6;
複製程式碼
- 連結串列轉紅黑樹所需要的最小表容量64,即當連結串列的長度達到轉紅黑樹的臨界值8的時候,如果表容量小於64,此時並不會把連結串列轉成紅黑樹,而會對錶進行擴容操作,減小連結串列的長度
static final int MIN_TREEIFY_CAPACITY = 64;
複製程式碼
- table,Node陣列
transient Node<K,V>[] table;
複製程式碼
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K,V>> entrySet;
複製程式碼
- 節點總數
transient int size;
複製程式碼
- 修改次數
transient int modCount;
複製程式碼
- 擴容閾值
int threshold;
複製程式碼
- 負載因子
final float loadFactor;
複製程式碼
結構
- Node結構,實現了Entry,hash值宣告為final,不再可變,即1.7中的rehash操作不存在了
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;
}
}
複製程式碼
建構函式
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);
}
複製程式碼
- 引數為Map的構造方法,先計算需要的容量大小,然後呼叫putVal方法插入節點
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
複製程式碼
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
複製程式碼
- tableSizeFor方法,給定初始化表容量值,返回表的實際初始化容量(必須是2的倍數),這個方法相比與1.7有了優化,更簡潔。
static final int tableSizeFor(int cap) {
int n = cap - 1; //先進行-1操作,當cap已經是2的倍數時,最後+1,返回該數本身
n |= n >>> 1; //右移1位,再進行或操作,然後賦值給n,使最高位的1的下一位也變成1
n |= n >>> 2; //右移2位,使最高2位的1右移覆蓋後2位的值,即最高4位均為1
n |= n >>> 4; //右移4位...
n |= n >>> 8; //右移8位...
n |= n >>> 16; //右移16位...
//如果cap<=0,返回1,如果>MAXIMUM_CAPACITY,返回MAXIMUM_CAPACITY,否則,最後的n+1操作返回大於等於cap的最小的2的倍數
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
複製程式碼
主要方法
- hash演算法進行了簡化,直接把hashCode()值的高16位移下來進行異或運算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製程式碼
put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
複製程式碼
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* onlyIfAbsent 如果是true,不存在才插入,存在則不改變原有的值
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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)
//如果table為null或者length為0,呼叫resize擴容方法(沒有單獨的///初始化方法了)
n = (tab = resize()).length;
//i = (n - 1) & //hash]計算hash值對應表中的位置,如果連結串列頭為null,直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果key存在,賦值給e,後面統一判斷是否插入
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果是樹節點,呼叫putTreeVal方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//迴圈tab[i = (n - 1) & //hash]上的連結串列,binCount記錄連結串列的長度,用來判斷是否轉化為樹結//構
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//如果key沒找到,直接插入
p.next = newNode(hash, key, value, null);
// -1 for 1st,如果長度達到了8,就轉化為樹結構
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;
}
}
if (e != null) {
// key存在,如果onlyIfAbsent為false,替換value,如果onlyIfAbsen//t 為true,原有值為null,也會替換,否則不變更原有值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); //LinkedHashMap重寫使用
return oldValue;
}
}
++modCount; //修改次數+1
if (++size > threshold) //如果size達到了擴容的閾值,則進行擴容操作
resize();
afterNodeInsertion(evict); //LinkedHashMap重寫用的
return null;
}
複製程式碼
get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
複製程式碼
- getNode方法很簡單,(n - 1) & hash計算key值對應的table下標,找到連結串列,先判斷頭節點,然後迴圈查詢,如果頭節點是樹節點,呼叫樹節點的getTreeNode方法
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 && // 先判斷第一個節點
((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;
}
複製程式碼
remove方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
複製程式碼
/**
* Implements Map.remove and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal //如果是true,value也要相等
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {//找到對應的頭節點
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//先判斷頭節點
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode) //如果是樹,呼叫樹的getTreeNode方法
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else { //迴圈連結串列
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) { //matchValue為true時,value也要相等才刪除節點
if (node instanceof TreeNode) //樹節點的刪除
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p) //如果是頭節點,把頭節點的下個節點賦值給頭節點
tab[index] = node.next;
else //把當前節點的next節點賦值給上一個節點的next(刪除當前節點)
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node); //空方法,LinkedHashMap重寫用
return node;
}
}
return null;
}
複製程式碼
resize方法(擴容)
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
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;
}
//如果容量大於等於16,並且*2小於上限,擴容2倍,新表容量=舊錶*2,新閾值=舊閾值*2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 初始化表,有參建構函式中把需要初始化的容量賦值給了threshold
newCap = oldThr;
else { // 如果沒有給定容量,預設初始化16,閾值16*0.75=12
newCap = 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;
/**
* 如果舊錶裡有值,需要把舊錶裡的值重新計算放到新表裡
* hash & (oldCap*2-1)計算新表中的位置,只可能得到兩種結果(把新表分成兩個小表)
* hash & (oldCap-1) 放在前面的表裡 和 hash & (oldCap-1) + oldCap 放在後面的表裡
* hash & oldCap == 0 就是第一種結果, !=0 就是第二種結果
*/
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) //頭節點是null,直接賦值
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode) //樹節點處理
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
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;
}
複製程式碼
多執行緒安全問題
HashMap不是執行緒安全的
- JDK1.7中,當兩個執行緒同時進行插入操作時,同時執行到createEntry方法時,獲取到了同一個頭節點e,第二個執行緒會覆蓋掉第一個執行緒的插入操作,使第一個執行緒插入的資料丟失。JDK1.8中的尾插法同樣會有這樣的問題,兩個執行緒獲取到相同的節點,然後把新鍵值對賦值給這個節點的next,後面的賦值操作覆蓋掉前面的。
- JDK1.7和JDK1.8中對map進行擴容時,由於節點的next會變化,造成實際有key值,但是讀操作返回null的情況。
- 1.7中,當兩個執行緒同時進行擴容操作時,可能會造成連結串列的死迴圈,形成過程:
- 現在有個map:
- 執行緒1進行擴容操作,執行transfer方法,賦值完節點e和next之後阻塞了。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
複製程式碼
- 執行緒2進行擴容操作並完成了擴容,建立了newTable2。
- 此時,節點e和next的連線情況如上圖所示,執行緒1如果繼續執行,執行過程如下:
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
複製程式碼
e = next;
Entry<K,V> next = e.next;
複製程式碼
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
```java
![](https://user-gold-cdn.xitu.io/2018/1/20/1611330db7c085de?w=668&h=444&f=png&s=149071)
```java
e = next;
Entry<K,V> next = e.next;
複製程式碼
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
複製程式碼
e = next;
複製程式碼
- 此時連結串列形成了死迴圈。
- 1.8中的transfer方法有了變化,不再倒置連結串列,所以不會造成死迴圈。
總結
- HashMap的結構,主要方法
- 1.7和1.8的區別
- 關於紅黑樹部分後面補充
歡迎大家關注我和小夥伴們的工作室號
(https://juejin.im/user/5a505bf5518825732a6d50ff/posts)