Hashmap在大廠面試中屬於必考的內容,今天花了一個下午看了它的源碼,不得不說,真的很經典。就看源碼這個問題來說吧,我和我的好基友又產生了隔閡。我認爲看hashmap的源碼是有必要的,它的底層有很多的優秀的數據結構的應用,比如說紅黑樹、哈希、數組+鏈表。雖然以前我就從很多網上經典的面經,瞭解它的一些內容,但總歸只是流於表面,認爲自己看的懂就行了,其實這也是學習上一個很大的誤區吧。一定要先自己嘗試的去IDEA中認真地從頭到尾的閱讀源碼,有自己的那部分理解並記錄下來,並且源碼一般都有相應的英文註釋,同時結合網上一些優秀的博客(開始是想從Java編程思想這本書來找到權威的內容,尷尬的是書中只是一筆帶過)進行全面的梳理。
HashMap的定義:
Implementation based on a hash table. (Use this instead of hashtable.) Provides constant-time performance for inserting and locating pairs. Performance can be adjusted via constructors that allow you to set the capacity and load factor of the hash table.
是以哈希數據結構實現的,用來作爲hashtable的代替品,在插入和定位元素時表現的很好,通過設置容量和裝載因子來調節大小。
HashMap的參數分析:
定義初始的容量大小,必須是2的倍數,默認的長度爲16
1static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
定義最大的容量大小,必須是2的29次方
1static final int MAXIMUM_CAPACITY = 1 << 30;
裝載因子默認爲0.75
1static final float DEFAULT_LOAD_FACTOR = 0.75f;
定義閾值爲8,當鏈表的長度爲大於這個閾值時,鏈表將會轉化爲紅黑樹。
1static final int TREEIFY_THRESHOLD = 8;
存儲元素的數據,必須是2的倍數
1transient Node<K,V>[] table;
臨界值,當實際大小大於臨界值時會進行擴容
threshold= tableSizeFor(initialCapacity)
1int threshold;
HashMap的數據結構
Node
1static class Node<K,V> implements Map.Entry<K,V> {
2final int hash;
3final K key;
4V value;
5Node<K,V> next;
6
7 Node(int hash, K key, V value, Node<K,V> next) {
8 this.hash = hash;
9 this.key = key;
10 this.value = value;
11 this.next = next;
12 }
TreeNode
1static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
2 TreeNode<K,V> parent; // red-black tree links
3 TreeNode<K,V> left;
4 TreeNode<K,V> right;
5 TreeNode<K,V> prev; // needed to unlink next upon deletion
6 boolean red;
7 TreeNode(int hash, K key, V val, Node<K,V> next) {
8 super(hash, key, val, next);
9 }
10
11 /**
12 * Returns root of tree containing this node.
13 */
14 final TreeNode<K,V> root() {
15 for (TreeNode<K,V> r = this, p;;) {
16 if ((p = r.parent) == null)
17 return r;
18 r = p;
19 }
20 }
構造方法
1public HashMap(int initialCapacity, float loadFactor) {
2if (initialCapacity < 0)
3throw new IllegalArgumentException("Illegal initial capacity: " +
4initialCapacity);
5if (initialCapacity > MAXIMUM_CAPACITY)
6initialCapacity = MAXIMUM_CAPACITY;
7if (loadFactor <= 0 || Float.isNaN(loadFactor))
8throw new IllegalArgumentException("Illegal load factor: " +
9loadFactor);
10this.loadFactor = loadFactor;
11this.threshold = tableSizeFor(initialCapacity);
12}
tableSizeFor方法
1static final int tableSizeFor(int cap) {
2int n = cap - 1;
3n |= n >>> 1;
4n |= n >>> 2;
5n |= n >>> 4;
6n |= n >>> 8;
7n |= n >>> 16;
8return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
9}
Hashmap擴容方法:
下面的代碼是JDK1.8中源碼,對1.7的擴容方法進行了改進,解決了在多線程下擴容出現死循環的情況,採用的方法是當鏈表的長度大於8時,就轉化成爲紅黑樹和使用尾插入數據。
1final Node<K,V>[] resize() {
2 Node<K,V>[] oldTab = table;
3 int oldCap = (oldTab == null) ? 0 : oldTab.length;
4 int oldThr = threshold;
5 int newCap, newThr = 0;
6 if (oldCap > 0) {
7 if (oldCap >= MAXIMUM_CAPACITY) {
8 threshold = Integer.MAX_VALUE;
9 return oldTab;
10 }
11 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
12 oldCap >= DEFAULT_INITIAL_CAPACITY)
13 newThr = oldThr << 1; // double threshold
14 }
15 else if (oldThr > 0) // initial capacity was placed in threshold
16 newCap = oldThr;
17 else { // zero initial threshold signifies using defaults
18 newCap = DEFAULT_INITIAL_CAPACITY;
19 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
20 }
21 if (newThr == 0) {
22 float ft = (float)newCap * loadFactor;
23 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
24 (int)ft : Integer.MAX_VALUE);
25 }
26 threshold = newThr;
27 @SuppressWarnings({"rawtypes","unchecked"})
28 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
29 table = newTab;
30 if (oldTab != null) {
31 for (int j = 0; j < oldCap; ++j) {
32 Node<K,V> e;
33 if ((e = oldTab[j]) != null) {
34 oldTab[j] = null;
35 if (e.next == null)
36 newTab[e.hash & (newCap - 1)] = e;
37 else if (e instanceof TreeNode)
38 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
39 else { // preserve order
40 Node<K,V> loHead = null, loTail = null;
41 Node<K,V> hiHead = null, hiTail = null;
42 Node<K,V> next;
43 do {
44 next = e.next;
45 if ((e.hash & oldCap) == 0) {
46 if (loTail == null)
47 loHead = e;
48 else
49 loTail.next = e;
50 loTail = e;
51 }
52 else {
53 if (hiTail == null)
54 hiHead = e;
55 else
56 hiTail.next = e;
57 hiTail = e;
58 }
59 } while ((e = next) != null);
60 if (loTail != null) {
61 loTail.next = null;
62 newTab[j] = loHead;
63 }
64 if (hiTail != null) {
65 hiTail.next = null;
66 newTab[j + oldCap] = hiHead;
67 }
68 }
69 }
70 }
71 }
72 return newTab;
73}
線程相關,爲啥線程不安全
在併發編程中使用HashMap可能導致程序死循環。在多線程環境下,使用HashMap進行put操作會引起死循環,導致CPU利用率接近100%,所以在併發情況下不能使用HashMap。例如,執行以下代碼會引起死循環。
1final HashMap<String, String> map = new HashMap<String, String>(2);
2 Thread t = new Thread(new Runnable() {
3 @Override
4 public void run() {
5 for (int i = 0; i < 10000; i++) {
6 new Thread(new Runnable() {
7 @Override
8 public void run() {
9 map.put(UUID.randomUUID().toString(), “”);
10}
11}, “ftf” + i).start();
12}
13}
14}, “ftf”);
15t.start();
16t.join();
HashMap在併發執行put操作時會引起死循環,是因爲多線程會導致HashMap的Entry鏈表形成環形數據結構,一旦形成環形數據結構,Entry的next節點永遠不爲空,就會產生死循環獲取Entry。
爲什麼不用hashtable?
HashTable容器使用synchronized來保證線程安全,但在線程競爭激烈的情況下HashTable的效率非常低下。因爲當一個線程訪問HashTable的同步方法,其他線程也訪問HashTable的同步方法時,會進入阻塞或輪詢狀態。如線程1使用put進行元素添加,線程2不但不能使用put方法添加元素,也不能使用get方法來獲取元素,所以競爭越激烈效率越低。
ConcurrentHashMap的實現原理,分段鎖怎麼做的
ConcurrentHashMap初始化方法是通過initialCapacity、loadFactor和concurrencyLevel等幾個
參數來初始化segment數組、段偏移量segmentShift、段掩碼segmentMask和每個segment裏的HashEntry數組來實現的。
ConcurrentHashMap是線程安全且高效的HashMap。HashTable容器在競爭激烈的併發環境下表現出效率低下的原因是所有訪問HashTable的
線程都必須競爭同一把鎖,假如容器裏有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼
當多線程訪問容器裏不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效提高並
發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術。首先將數據分成一段一段地存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重入鎖(ReentrantLock),在ConcurrentHashMap裏扮演鎖的角色;HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap裏包含一個Segment數組。Segment的結構和HashMap類似,是一種數組和鏈表結構。一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,每個Segment守護着一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先獲得與它對應的Segment鎖。