本篇文章包括:
- 資料結構
- 各個引數
- 為什麼陣列的長度是2的整數次方
- 為什麼要將裝載因子定義為0.75
- 為什麼連結串列轉紅黑樹的閾值為8
- hash碰撞
- put方法
- resize方法
- jdk7中陣列擴容產生環的問題。
1.底層資料結構?
- 紅黑樹
是一種接近二叉平衡樹的資料結構,有5個性質:
-
性質1:每個節點要麼是黑色,要麼是紅色。
-
性質2:根節點是黑色。
-
性質3:每個葉子節點(NIL||null)是黑色(為空的葉子結點)。
-
性質4:每個紅色結點的兩個子結點一定都是黑色。
-
性質5:任意一結點到每個葉子結點的路徑都包含數量相同的黑結點。(保證了紅黑樹的平衡性)
紅黑樹的查詢效率高,時間複雜度為O(logn),但是新增節點的代價高,因為本身需要保證平衡,方法包括左旋、右旋以及變色。
- 各個引數
/**
預設的初始容量
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**.
最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
裝載因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
連結串列轉紅黑樹閾值
*/
static final int TREEIFY_THRESHOLD = 8;
/**
紅黑樹轉連結串列閾值
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
為避免調整大小和調整樹型閾值之間的衝突,可以重新調整儲存箱的最小表容量(如果儲存箱中的節點太多,則重新調整表 的大小)應至少為4個樹型閾值。
為了避免進行擴容、樹形化選擇的衝突,規定若桶內的節點的數量大於64則進行擴容,否則進行樹形化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
初始容量為什麼是16或者說2的次方數
我們先看看2的次方數:
十進位制數 | 二進位制數 | |
---|---|---|
2 | 0010 | |
4 | 0100 | |
8 | 1000 | |
16 | 0001 0000 |
發現2的整數次方的數的二進位制剛好都是最高位為1,那又有什麼用呢?這就要說說hashMap的put方法了額。
hashMap通過 (n - 1) & hash來計算鍵值對存放的陣列下標,可以自己嘗試計算一下發現如果n是2的整數次方數的話那麼就和n%hash的值一樣,也就是說是為了保證計算後的結果(作為下標)不超出陣列長度減一,從而找到對應的儲存位置。
public V put(K key, V value) {
//先計算key的hash值,然後呼叫putAal
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//如果陣列長度為0,就進行初始化容量預設為16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果當前陣列的這個位置沒有元素就直接賦值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//否則有以下幾種新增節點
Node<K,V> e; K k;
//當前的節點的hash值、key相等就進行覆蓋
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果當前是為紅黑樹結構就加入到紅黑樹中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//當前位置已經存在元素,並且是連結串列結構就加入節點
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//連結串列中有key相同的節點
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//將舊值替換為新值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//元素個數加一,並且判斷是否需要擴容,若大於裝載因子*陣列長度就進行擴容
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
//hash值計算方法
static final int hash(Object key) {
int h;
//可以看出允許key為null,hashCode是一個本地方法
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- hash碰撞:如果計算出來的最終的值(也就是要放到的那個陣列下標的位置)對應的位置有元素就產生hash碰撞,解決辦法有開放地址法,在雜湊法,鏈地址(拉鍊)法以及通過建立公共的溢位區來解決。hashMap是使用的鏈地址法。通過尾插法插入到當前連結串列的尾部(jdk7採用的頭插法會導致擴容的時候產生連結串列環的問題)。
裝載因子
- 預設為0.75,為了在時間和空間上進行折中。如果小了就有可能造成空間的浪費,大了又會產生更多的hash碰撞,造成執行時間增加。
連結串列轉紅黑樹,以及紅黑樹轉連結串列
- 當連結串列的長度達到8的時候會轉為紅黑樹結構,因為連結串列的查詢效率低,如果連結串列過長就會造成查詢時間過長,而紅黑樹結構的查詢效率較高,但是進行增加元素的時候效率較低。當元素的個數為6的時候紅黑樹結構又會轉為連結串列結構。
- 為什麼會將閾值定為8?jdk官方解釋:
Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
總之就是在8的時候再產生插入的操作的概率非常小,因為紅黑樹的增加節點的效率是很低的,不該有過多的增加節點的操作。
看看resize方法
final Node<K,V>[] resize() {
//舊陣列
Node<K,V>[] oldTab = table;
//舊陣列容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//舊陣列的擴容閾值
int oldThr = threshold;
//新陣列的大小,擴容閾值
int newCap, newThr = 0;
//當舊陣列長度不為0
if (oldCap > 0) {
//舊陣列的長度已經為最大了就不進行擴容,直接將閾值賦值為最大
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//對陣列的容量和閾值擴大為原來的兩倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//當陣列大小為0的時候對陣列進行初始化,後面會對threshold進行處理,因為閾值是裝載因子與陣列的長度的乘積
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else {
// zero initial threshold signifies using defaults
//使用無參構造進行new陣列,第一次put的時候會對陣列進行預設的初始化
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;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//如果當前下標有元素,有以下幾種情況
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
///1.如果當前元素沒有後繼元素,則直接進行hash計算下標將節點放在新陣列對應的下標處
if (e.next == 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;
//如果當前元素的hash值與舊陣列進行與運算得到0則用低位記錄
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;
}
- 為什麼jdk7中擴容會產生環的問題
看resize方法:
//擴容
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, 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; //1
if (rehash) {//如果是重新Hash,則需要重新計算hash值
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);//定位Hash桶
e.next = newTable[i];//2
newTable[i] = e;//newTable[i]的值總是最新插入的值
e = next;//繼續下一個元素
}
}
}
- 分析擴容的過程
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;
}
- 兩個執行緒同時進行擴容(假設擴容後的元素在陣列中的下標還是原來的下標),假設執行緒1先進行
- 執行緒1擴容完畢後連結串列的順序已經倒置:
- 執行緒2進行擴容的時候就形成了環形連結串列:
由於執行緒2中存放的han1的next還指向著han2,所以導致環形連結串列的產生。
jdk8中採用尾插法避免了這個問題,通過採用高位指標和低位指標來進行連結串列元素的轉移,巧妙的避開了環形連結串列的問題。