1、HashMap 線程不安全
初始化:構造一個空的HashMap,初始容量爲16,負載因子爲0.75
(1.7 Entry<K,V>頭插法 1.8Node<K,V>尾插法)
擴容:經過HashMap源碼能夠看到是在put操做時,即向容器中添加元素時,判斷當前容器中元素的個數是否達到閾值(當前數組長度乘以加載因子的值)的時候,就要自動擴容了。 其實就是從新計算容量;而這個擴容是計算出所需容器的大小以後從新定義一個新的容器,將原來容器中的元素放入其中。
設定threshold。 這個threshold = capacity * load factor 。當HashMap的size到了threshold時,就要進行resize,也就是擴容。
tableSizeFor()的主要功能是返回一個比給定整數大且最接近的2的冪次方整數,如給定10,返回2的4次方16.
注:hash 衝突發生的幾種狀況:
1.兩節點key 值相同(hash值必定相同),致使衝突;
2.兩節點key 值不一樣,因爲 hash 函數的侷限性致使hash 值相同,衝突;
3.兩節點key 值不一樣,hash 值不一樣,但 hash 值對數組長度取模後相同,衝突;
//實現put和相關方法。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//若是table爲空或者長度爲0,則resize()
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//肯定插入table的位置,算法是(n - 1) & hash,在n爲2的冪時,至關於取摸操做。
////找到key值對應的槽而且是第一個,直接加入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//在table的i位置發生碰撞,有兩種狀況,一、key值是同樣的,替換value值,
//二、key值不同的有兩種處理方式:2.一、存儲在i位置的鏈表;2.二、存儲在紅黑樹中
else {
Node<K,V> e; K k;
//第一個node的hash值即爲要加入元素的hash
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//2.2
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//2.1
else {
//不是TreeNode,即爲鏈表,遍歷鏈表
for (int binCount = 0; ; ++binCount) {
///鏈表的尾端也沒有找到key值相同的節點,則生成一個新的Node,
//而且判斷鏈表的節點個數是否是到達轉換成紅黑樹的上界達到,則轉換成紅黑樹。
if ((e = p.next) == null) {
// 建立鏈表節點並插入尾部
p.next = newNode(hash, key, value, null);
////超過了鏈表的設置長度8就轉換成紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//若是e不爲空就替換舊的oldValue值
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;
}
1.7和1.8的HashMap的不一樣點
(1)JDK1.7用的是頭插法,而JDK1.8及以後使用的都是尾插法,那麼爲何要這樣作呢?由於JDK1.7是用單鏈表進行的縱向延伸,當採用頭插法就是可以提升插入的效率,可是也會容易出現逆序且環形鏈表死循環問題。可是在JDK1.8以後是由於加入了紅黑樹使用尾插法,可以避免出現逆序且鏈表死循環的問題。
(2)擴容後數據存儲位置的計算方式也不同:
-
在JDK1.7的時候是直接用hash值和須要擴容的二進制數進行&(這裏就是爲何擴容的時候爲啥必定必須是2的多少次冪的緣由所在,由於若是隻有2的n次冪的狀況時最後一位二進制數才必定是1,這樣能最大程度減小hash碰撞)(hash值 & length-1) 。
-
而在JDK1.8的時候直接用了JDK1.7的時候計算的規律,也就是擴容前的原始位置+擴容的大小值=JDK1.8的計算方式,而再也不是JDK1.7的那種異或的方法。可是這種方式就至關於只須要判斷Hash值的新增參與運算的位是0仍是1就直接迅速計算出了擴容後的儲存方式。
(3)JDK1.7的時候使用的是數組+ 單鏈表的數據結構。可是在JDK1.8及以後時,使用的是數組+鏈表+紅黑樹的數據結構(當鏈表的深度達到8的時候,也就是默認閾值,就會自動擴容把鏈表轉成紅黑樹的數據結構來把時間複雜度從O(N)變成O(logN)提升了效率)。
HashMap爲何是線程不安全的?
HashMap 在併發時可能出現的問題主要是兩方面:
-
put的時候致使的多線程數據不一致
好比有兩個線程A和B,首先A但願插入一個key-value對到HashMap中,首先計算記錄所要落到的 hash桶的索引座標,而後獲取到該桶裏面的鏈表頭結點,此時線程A的時間片用完了,而此時線程B被調度得以執行,和線程A同樣執行,只不過線程B成功將記錄插到了桶裏面,假設線程A插入的記錄計算出來的 hash桶索引和線程B要插入的記錄計算出來的 hash桶索引是同樣的,那麼當線程B成功插入以後,線程A再次被調度運行時,它依然持有過時的鏈表頭可是它對此一無所知,以致於它認爲它應該這樣作,如此一來就覆蓋了線程B插入的記錄,這樣線程B插入的記錄就憑空消失了,形成了數據不一致的行爲。
-
resize而引發死循環
這種狀況發生在HashMap自動擴容時,當2個線程同時檢測到元素個數超過 數組大小 × 負載因子。此時2個線程會在put()方法中調用了resize(),兩個線程同時修改一個鏈表結構會產生一個循環鏈表(JDK1.7中,會出現resize先後元素順序倒置的狀況)。接下來再想經過get()獲取某一個元素,就會出現死循環。
HashMap和HashTable的區別
HashMap和Hashtable都實現了Map接口,但決定用哪個以前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。
-
HashMap幾乎能夠等價於Hashtable,除了HashMap是非synchronized的,並能夠接受null(HashMap能夠接受爲null的鍵值(key)和值(value),而Hashtable則不行)。
-
HashMap是非synchronized,而Hashtable是synchronized,這意味着Hashtable是線程安全的,多個線程能夠共享一個Hashtable;而若是沒有正確的同步的話,多個線程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴展性更好。
-
另外一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。因此當有其它線程改變了HashMap的結構(增長或者移除元素),將會拋出ConcurrentModificationException,但迭代器自己的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並非一個必定發生的行爲,要看JVM。這條一樣也是Enumeration和Iterator的區別。
-
因爲Hashtable是線程安全的也是synchronized,因此在單線程環境下它比HashMap要慢。若是你不須要同步,只須要單一線程,那麼使用HashMap性能要好過Hashtable。
-
HashMap不能保證隨着時間的推移Map中的元素次序是不變的。
須要注意的重要術語:
-
sychronized意味着在一次僅有一個線程可以更改Hashtable。就是說任何線程要更新Hashtable時要首先得到同步鎖,其它線程要等到同步鎖被釋放以後才能再次得到同步鎖更新Hashtable。
-
Fail-safe和iterator迭代器相關。若是某個集合對象建立了Iterator或者ListIterator,而後其它的線程試圖「結構上」更改集合對象,將會拋出ConcurrentModificationException異常。但其它線程能夠經過set()方法更改集合對象是容許的,由於這並無從「結構上」更改集合。可是假如已經從結構上進行了更改,再調用set()方法,將會拋出IllegalArgumentException異常。
-
結構上的更改指的是刪除或者插入一個元素,這樣會影響到map的結構。
HashMap能夠經過下面的語句進行同步:
Map m = Collections.synchronizeMap(hashMap);
1.1 HashMap()
// 1.無參構造方法、
// 構造一個空的HashMap,初始容量爲16,負載因子爲0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
無參構造方法就沒什麼好說的了。
1.2 HashMap(int initialCapacity)
2.構造一個初始容量爲initialCapacity,負載因子爲0.75的空的HashMap,
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
HashMap(int initialCapacity) 這個構造方法調用了1.3中的構造方法。
1.3 HashMap(int initialCapacity, float loadFactor)
// 3.構造一個空的初始容量爲initialCapacity,負載因子爲loadFactor的HashMap
//最大容量
//static final int MAXIMUM_CAPACITY = 1 << 30;
當指定的初始容量< 0時拋出IllegalArgumentException異常,當指定的初始容量> MAXIMUM_CAPACITY時,就讓初始容量 = MAXIMUM_CAPACITY。當負載因子小於0或者不是數字時,拋出IllegalArgumentException異常。
設定threshold。 這個threshold = capacity * load factor 。當HashMap的size到了threshold時,就要進行resize,也就是擴容。
tableSizeFor()的主要功能是返回一個比給定整數大且最接近的2的冪次方整數,如給定10,返回2的4次方16.
遍歷
無法經過索引獲取因此不能用for循環
能夠用加強for循環 hashMap.entrySet();的時候new EntrySet(),裏面有foreach循環遍歷map
或者 hashMap.entrySet().iterator() 返回EntryIterator()extends HashIterator()
裏面的next() 調用了HashIterator的nextNode()
1.數據結構
Hashtable底層是經過數組加鏈表來實現的。(Entry<K,V>)
2.初始化(初始化容量11)
Hashtable並無太多的常量,好比默認容量大小都是直接寫在代碼中,而沒使用常量。
從它的構造函數咱們能夠知道,Hashtable默認capacity是11,默認負載因子是0.75.
它的key、value都不能夠爲null。
3.擴容
hash數組默認大小是11,擴充方式是old*2+1
4.線程安全性
線程安全(concurrenthashmap更好)
5.遍歷
6.插入刪除
Hashtable是在鏈表的頭部添加元素的,而HashMap是尾部添加的(1.7頭插,1.8尾插)
3、LinkedHashMap
-
核心
-
LinkedHashMap是繼承於HashMap,是基於HashMap和雙向鏈表來實現的。
-
HashMap無序;LinkedHashMap有序,可分爲插入順序和訪問順序兩種。若是是訪問順序,那put和get操做已存在的Entry時,都會把Entry移動到雙向鏈表的表尾(實際上是先刪除再插入)。
-
LinkedHashMap存取數據,仍是跟HashMap同樣使用的Entry[]的方式,雙向鏈表只是爲了保證順序。
-
LinkedHashMap是線程不安全的。
LinkedHashMap是有序的,且默認爲插入順序
LinkedHashMap提供了多個構造方法,咱們先看空參的構造方法。
public LinkedHashMap() {
// 調用HashMap的構造方法,其實就是初始化Entry[] table
super();
// 這裏是指是否基於訪問排序,默認爲false
accessOrder = false;
}
首先使用super調用了父類HashMap的構造方法,其實就是根據初始容量、負載因子去初始化Entry[] table,詳細的看上一篇
HashMap解析。
而後把accessOrder設置爲false,這就跟存儲的順序有關了,LinkedHashMap存儲數據是有序的,並且分爲兩種:插入順序和訪問順序。
這裏accessOrder設置爲false,表示不是訪問順序而是插入順序存儲的,這也是默認值,表示LinkedHashMap中存儲的順序是按照調用put方法插入的順序進行排序的。
經過get方法,致使key爲name1對應的Entry到表尾
數據結構(1.7繼承Map.Entry<K,V>
),比HashMap多了before, after;
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
LinkedHashMap構造函數,主要就是調用HashMap構造函數初始化了一個Entry[] table,而後調用自身的init初始化了一個只有頭結點的雙向鏈表。
LinkedHashMap就是HashMap+雙向鏈表,下面用圖來表示逐步往LinkedHashMap中添加數據的過程,紅色部分是雙向鏈表,黑色部分是HashMap結構,header是一個Entry類型的雙向鏈表表頭,自己不存儲數據。
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
// 把新建立的Entry,加入到雙向鏈表中
e.addBefore(header);
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
一直是一個環(新添加的鏈接header)
put( 實際上是先刪除,再插入)
舉個栗子:開始時,HashMap中有Entry一、Entry二、Entry3,並設置LinkedHashMap爲訪問順序,則更新Entry1時,會先把Entry1從雙向鏈表中刪除,而後再把Entry1加入到雙向鏈表的表尾,而Entry1在HashMap結構中的存儲位置沒有變化
get( 實際上是先刪除,再插入)
首先經過key算出hash值,而後根據hash值算出在table中存儲的index,而後遍歷table[index]的單向鏈表去對比key,若是找到了就返回Entry。
後面調用了LinkedHashMap.Entry的recordAccess方法,上面分析過put過程當中這個方法,其實就是在訪問順序的LinkedHashMap進行了get操做之後,從新排序,把get的Entry移動到雙向鏈表的表尾。
在上一篇HashMap中就分析了remove過程,其實就是斷開其餘對象對本身的引用。好比被刪除Entry是在單向鏈表的表頭,則讓它的next放到表頭,這樣它就沒有被引用了;若是不是在表頭,它是被別的Entry的next引用着,這時候就讓上一個Entry的next指向它本身的next,這樣,它也就沒被引用了。
在HashMap.Entry中recordRemoval方法是空實現,可是LinkedHashMap.Entry對其進行了重寫,以下:
void recordRemoval(HashMap<K,V> m) {
remove();
}
private void remove() {
before.after = after;
after.before = before;
}
易知,這是要把雙向鏈表中的Entry刪除,也就是要斷開當前要被刪除的Entry被其餘對象經過after和before的方式引用
4、TreeMap
TreeMap 默認排序規則:按照key的字典順序來排序(升序)
固然,也能夠自定義排序規則:要實現Comparator接口。
Comparator<test> comparator = new Comparator<test>() {
@Override
public int compare(test o1, test o2) {
// 自定義規則返回-1 0 1
return 0;
}
};
new TreeMap<>(comparator);
(01) TreeMap實現繼承於AbstractMap,而且實現了NavigableMap接口。
(02) TreeMap的本質是R-B Tree(紅黑樹),它包含幾個重要的成員變量: root, size, comparator。
root 是紅黑數的根節點。它是Entry類型,Entry是紅黑數的節點,它包含了紅黑數的6個基本組成成分:key(鍵)、value(值)、left(左孩子)、right(右孩子)、parent(父節點)、color(顏色)。Entry節點根據key進行排序,Entry節點包含的內容爲value。
紅黑數排序時,根據Entry中的key進行排序;Entry中的key比較大小是根據比較器comparator來進行判斷的。
size是紅黑數中節點的個數。
1.1 紅黑樹的節點顏色--紅色
private static final boolean RED = false;
1.2 紅黑樹的節點顏色--黑色
private static final boolean BLACK = true;
1.3 「紅黑樹的節點」對應的類。
static final class Entry<K,V> implements Map.Entry<K,V> { ... }
Entry包含了6個部份內容:key(鍵)、value(值)、left(左孩子)、right(右孩子)、parent(父節點)、color(顏色)
Entry節點根據key進行排序,Entry節點包含的內容爲value。
2 相關操做
2.1 左旋
private void rotateLeft(Entry<K,V> p) { ... }
2.2 右旋
private void rotateRight(Entry<K,V> p) { ... }
2.3 插入操做
public V put(K key, V value) { ... }
2.4 插入修正操做
紅黑樹執行插入操做以後,要執行「插入修正操做」。
目的是:保紅黑樹在進行插入節點以後,仍然是一顆紅黑樹
private void fixAfterInsertion(Entry<K,V> x) { ... }
2.5 刪除操做
private void deleteEntry(Entry<K,V> p) { ... }
2.6 刪除修正操做
紅黑樹執行刪除以後,要執行「刪除修正操做」。
目的是保證:紅黑樹刪除節點以後,仍然是一顆紅黑樹
private void fixAfterDeletion(Entry<K,V> x) { ... }