hashmap-大廠必備

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,Vimplements 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,Vextends 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鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章