廢話不多說,直接進入主題:
首先我們從構造方法開始:
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, 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);
// 初始化加載因子(默認0.75f)
this.loadFactor = loadFactor;
// 初始化容器大小(默認16)
threshold = initialCapacity;
init();
}
// 可以看到jdk1.7中hashMap的init方法並沒有創建hashMap的數組和Entry,
// 而是移到了put方法裏,後邊會講到
void init() {
}
最常用的put
方法:
public V put(K key, V value) {
// 可以看到,初始化table是在首次put時開始的
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 對key爲`null`的處理,進入到方法裏可以看到直接將其hash置爲0,並插入到了數組下標爲0的位置
if (key == null)
return putForNullKey(value);
// 計算hash值
int hash = hash(key);
// 根據hash,查找到數組對應的下標
int i = indexFor(hash, table.length);
// 遍歷數組第i個位置的鏈表
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 找到相同的key,並覆蓋其value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 在table[i]下的鏈表中沒有找到相同的key,將entry加入到此鏈表
// addEntry方法後邊會再看一下
addEntry(hash, key, value, i);
return null;
}
根據put
方法的流程,我們進入到inflateTable
方法看一下他的初始化代碼:
// 容量一定爲2的n次方,比如設置size=10,則容量則爲大於10的且爲2的n次方=16
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
// 計算擴容臨界值:capacity * loadFactor,當size>=threshold時,觸發擴容
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 初始化Entry數組
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
addEntry
添加鏈表節點
能進入到addEntry
方法,說明根據hash值計算出的數組下標衝突,但是key不一樣
void addEntry(int hash, K key, V value, int bucketIndex) {
// 當數組的size >= 擴容閾值,觸發擴容,size大小會在createEnty和removeEntry的時候改變
if ((size >= threshold) && (null != table[bucketIndex])) {
// 擴容到2倍大小,後邊會跟進這個方法
resize(2 * table.length);
// 擴容後重新計算hash和index
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
// 創建一個新的鏈表節點,點進去可以瞭解到是將新節點添加到了鏈表的頭部
createEntry(hash, key, value, bucketIndex);
}
resize
擴容
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 創建2倍大小的新數組
Entry[] newTable = new Entry[newCapacity];
// 將舊數組的鏈表轉移到新數組,就是這個方法導致的hashMap不安全,等下我們進去看一眼
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
// 重新計算擴容閾值(容量*加載因子)
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
get
方法
對於put方法,get方法就很簡單了
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
// 根據hash值找到對應的數組下標,並遍歷其E
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;
}
不安全的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);
}
// 計算節點在新數組中的下標
int i = indexFor(e.hash, newCapacity);
// 將舊節點插入到新節點的頭部
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
這裏粗略的講一下爲什麼transfer
是不安全的
- 從上面的代碼可以看出,從oldTable中遍歷Entry是正序的,也就是
a->b->c
的順序,而插入到新數組的時候是採用的頭插法,也就是後插入的在首部,所以遍歷之後結果爲c->b->a
; - 此時正常邏輯是沒有問題的,而當有多個線程同時進行擴容操作時就出現問題了,看下邊的圖
此時的狀態爲a線程創建了新數組,b線程也創建了新數組,同時b的cpu時間片用完進入等待階段,
此時的狀態爲a線程完成了數組的擴容,退出了transfer
方法,但是還沒有執行下一句table = newTable;
b線程回來繼續執行代碼
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
結果如下:
b會繼續執行循環代碼,進入到死循環狀態。
關於transfer
不安全的問題,感興趣的可以去看一下這篇文章老生常談,HashMap的死循環。