如何存儲數據 (put、get)算法
put數據數組
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == 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))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //將算出的key的hash值,key,value ,以及索引存放在entry對象中,並加入table數組中 addEntry(hash, key, value, i); return null; }
get數據多線程
public V get(Object key) { //若是key爲null,則返回null對於的value if (key == null) return getForNullKey(); //根據key獲取entry Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } //計算出key的hash值 int hash = (key == null) ? 0 : hash(key); //根據hash值獲取索引,遍歷索引下的全部的entry值 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; //若是hash值相同而且key也相同則返回value if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
默認容量爲何是16this
//1前移4位 二進制1是 01 ,前移4位 則是 10000,即16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
咱們先看一下源碼如何獲取index的值線程
static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; //hash 值與 數組的長度-1進行位運算,長度必須是2的冪。 return h & (length-1); }
index的值是經過key的hash值與數組長度-1進行位運算來的。舉個例子,code
計算book的hashcode,結果爲十進制的3029737,二進制的101110001110101110 1001;對象
HashMap長度是默認的16,計算Length-1的結果爲十進制的15,二進制的1111blog
把以上兩個結果作與運算,101110001110101110 1001 & 1111 = 1001,十進制是9,因此 index=9
能夠說,Hash算法最終獲得的index結果,徹底取決於Key的Hashcode值的最後幾位。索引
==長度16或者其餘2的冪,Length-1的值是全部二進制位全爲1,這種狀況下,index的結果等同於HashCode後幾位的值。只要輸入的HashCode自己分佈均勻,Hash算法的結果就是均勻的。==ci
負載因子爲何是0.75f
/** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
加載因子須要在時間和空間成本上尋求一種折衷。
加載因子太高,例如爲1,雖然減小了空間開銷,提升了空間利用率,但同時也增長了查詢時間成本
加載因子太低,例如0.5,雖然能夠減小查詢時間成本,可是空間利用率很低,同時提升了rehash操做的次數。
hash衝突的產生與解決
產生
例如咱們如今要加入一個key=A,value=a,先須要計算A的index,假如是2。那麼結果以下:
圖1
再加入一個key=A', value=a',此時恰好計算A'的index也是2,這時候就會產生衝突。
解決hash衝突
這個時候咱們須要使用鏈表來解決hash衝突,當產生相同的index是,則經過Next指向同一個index的下一個節點,如圖:
(
圖二
)
注意:新來的Entry節點插入鏈表時,使用的是「頭插法」。至於爲何不插入鏈表尾部,由於HashMap的做者發現後插入的Entry被查找的可能性更大
何時會出現死鎖
當擴容時,須要將老的table的數據轉移到新的table上,調用transfer發放進行轉移。
咱們先看下transfer方法
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //循環遍歷老的table for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; //判斷是否須要從新計算hash值 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; } } }
正常狀況下,轉移前鏈表順序是1->2->3,逆序轉移後新的t順序變成 3->2->1,可是在多線程環境中,使用HashMap進行put操做時會引發死循環,致使死循環的緣由是在多線程的狀況下,會造成環形鏈表,這時是沒有問題的,咱們再看看get()方法中的獲取Entry的方法
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; }
加入咱們執行get方法找一個不存在的值的時候,for循環將會死循環,即會造成死鎖