精進之路之HashMap

HashMap本質的核心就是「數組+鏈表」,數組對於訪問速度很快,而鏈表的優點在於插入速度快,HashMap集兩者於一身。html

提到HashMap,咱們不得不提各個版本對於HashMap的不一樣。本文中先從1.6版本談起,分別從結構,hash,擴容等幾方面展開來看。在具體討論以前,咱們先了解下HashMap的結構:java

JDK1.6之結構:
算法

 從圖中咱們能夠看到一個hashmap就是一個數組結構,當新建一個hashmap的時候,就會初始化一個數組。咱們來看看java代碼:數組

 

/**
* The table, resized as necessary. Length MUST Always be a power of two.
* 表,根據須要調整大小。長度必須是2的冪
*/
transient Entry[] table;

static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //當前的key
V value;//當前的value
Entry<K,V> next;//下一個元素
final int hash;// hash值

/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
......
}

上面的Entry就是數組中的元素,它持有一個指向下一個元素的引用,這就構成了鏈表。        當咱們往hashmap中put元素的時候,先根據key的hash值獲得這個元素在數組中的位置(即下標),而後就能夠把這個元素放到對應的位置中了。
若是這個元素所在的位子上已經存放有其餘元素了,那麼在同一個位子上的元素將以鏈表的形式存放,新加入的放在鏈頭,最早加入的放在鏈尾。
從hashmap中get元素時,首先計算key的hashcode,找到數組中對應位置的某一元素,而後經過key的equals方法在對應位置的鏈表中找到須要的元素。
從這裏咱們能夠想象獲得,若是每一個位置上的鏈表只有一個元素,那麼hashmap的get效率將是最高的。

JDK1.6之hash算法:
咱們能夠看到在hashmap中要找到某個元素,須要根據key的hash值來求得對應數組中的位置。如何計算這個位置就是hash算法。
前面說過hashmap的數據結構是數組和鏈表的結合,因此咱們固然但願這個hashmap裏面的元素位置儘可能的分佈均勻些,儘可能使得每一個位置上的元素數量只有一個,
那麼當咱們用hash算法求得這個位置的時候,立刻就能夠知道對應位置的元素就是咱們要的,而不用再去遍歷鏈表。
因此咱們首先想到的就是把hashcode對數組長度取模運算,這樣一來,元素的分佈相對來講是比較均勻的。
可是,「模」運算的消耗仍是比較大的,能不能找一種更快速,消耗更小的方式那?java中時這樣作的,
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}


首先算得key得hashcode值,而後跟數組的長度-1作一次「與」運算(&)。看上去很簡單,其實比較有玄機。
好比數組的長度是2的4次方,那麼hashcode就會和2的4次方-1作「與」運算。
不少人都有這個疑問,爲何hashmap的數組初始化大小都是2的次方大小時,hashmap的效率最高
我以2的4次方舉例,來解釋一下爲何數組大小爲2的冪時hashmap訪問的性能最高。         看下圖,左邊兩組是數組長度爲16(2的4次方),右邊兩組是數組長度爲15。兩組的hashcode均爲8和9,可是很明顯,當它們和1110「與」的時候,
產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8和9會被放到同一個鏈表上,那麼查詢的時候就須要遍歷這個鏈表,
獲得8或者9,這樣就下降了查詢的效率。同時,咱們也能夠發現,當數組長度爲15的時候,hashcode的值會與14(1110)進行「與」,
那麼最後一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費至關大,
更糟的是這種狀況中,數組可使用的位置比數組長度小了不少,這意味着進一步增長了碰撞的概率,減慢了查詢的效率!


 因此說,當數組長度爲2的n次冪的時候,不一樣的key算得得index相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
          說到這裏,咱們再回頭看一下hashmap中默認的數組大小是多少,查看源代碼能夠得知是16,爲何是16,而不是15,也不是20呢,看到上面annegu的解釋以後咱們就清楚了吧,顯然是由於16是2的整數次冪的緣由,在小數據量的狀況下16比15和20更能減小key之間的碰撞,而加快查詢的效率。

因此,在存儲大容量數據的時候,最好預先指定hashmap的size爲2的整數次冪次方。就算不指定的話,也會以大於且最接近指定值大小的2次冪來初始化的,代碼以下(HashMap的構造方法中):
數據結構

// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;


JDK1.6之resize(默認擴充爲原來的兩倍):
/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}

Entry[] newTable = new Entry[newCapacity];
transfer(newTable);//轉換新表
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}

/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable) {
Entry[] src = table;//擴容前
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {//進行老entry遍歷
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
       當hashmap中的元素愈來愈多的時候,碰撞的概率也就愈來愈高(由於數組的長度是固定的),
因此爲了提升查詢的效率,就要對hashmap的數組進行擴容,數組擴容這個操做也會出如今ArrayList中,
因此這是一個通用的操做,不少人對它的性能表示過懷疑,不過想一想咱們的「均攤」原理,就釋然了,
而在hashmap數組擴容以後,最消耗性能的點就出現了:原數組中的數據必須從新計算其在新數組中的位置,並放進去,這就是resize。          那麼hashmap何時進行擴容呢?當hashmap中的元素個數超過數組大小*loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,
也就是說,默認狀況下,數組大小爲16,那麼當hashmap中元素個數超過16*0.75=12的時候,就把數組的大小擴展爲2*16=32,即擴大一倍,而後從新計算每一個元素在數組中的位置,
而這是一個很是消耗性能的操做,因此若是咱們已經預知hashmap中元素的個數,那麼預設元素的個數可以有效的提升hashmap的性能。好比說,咱們有1000個元素new HashMap(1000), 可是理論上來說new HashMap(1024)更合適,不過上面annegu已經說過,即便是1000,hashmap也自動會將其設置爲1024。 可是new HashMap(1024)還不是更合適的,由於0.75*1000 < 1000, 也就是說爲了讓0.75 * size > 1000, 咱們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了resize的問題。

1.7多線程

 
  • 加入了jdk.map.althashing.threshold這個jdk的參數用來控制是否在擴容時使用String類型的新hash算法。
  • 把1.6的構造方法中對錶的初始化挪到了put方法中。
  • 1.6中的tranfer方法對舊錶的節點進行置null操做(存在多線程問題),1.7中去掉了。
 

1.8less

 

hashmap有了重大更新,其內部實現採用了紅黑樹,entry鏈表長度超過閾值8,就會轉爲樹結構,性能有了較大提高。性能

 

ConcurrentHashMap一樣進行了巨大更新,放棄使用以前的分區鎖,而是使用CAS原子操做來提供修改樹節點的原子操做,其鎖的粒度實際是節點,this

故性能比之前有了很多的提高。和hashmap同樣採用樹結構,可是樹的根節點是不同的,也就是數組節點不同。spa

 

注意: resize 發生在大於等於臨界值,而不僅僅是大於臨界值,如下代碼爲例:當前size先進性了自增1操做,故size=threshold 時,便會發生resize()

 

 

 注:本文摘自、整理以下文章,感謝原做者的傾心分享:http://www.iteye.com/topic/539465

推薦相關文章:http://www.importnew.com/28263.html

相關文章
相關標籤/搜索