廢話很少說,直接進入主題:java
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
是不安全的 a->b->c
的順序,而插入到新數組的時候是採用的頭插法,也就是後插入的在首部,因此遍歷以後結果爲c->b->a
;此時的狀態爲a線程建立了新數組,b線程也建立了新數組,同時b的cpu時間片用完進入等待階段,post
此時的狀態爲a線程完成了數組的擴容,退出了transfer
方法,可是尚未執行下一句table = newTable;
this
b線程回來繼續執行代碼spa
Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next;
結果以下:線程
b會繼續執行循環代碼,進入到死循環狀態。3d
關於transfer
不安全的問題,感興趣的能夠去看一下這篇文章老生常談,HashMap的死循環。code