1.HashMap的源碼,實現原理,JDK8對HashMap作了怎樣的優化?html
2.HaspMap擴容是怎樣擴容的,爲何都是2的N次冪的大小。 redis
3.HashMap,HashTable,ConcurrentHashMap的區別。算法
4.極高併發下HashTable和ConcurrentHashMap哪一個性能更好,爲何,如何實現的。數組
5.HashMap在高併發下若是沒有處理線程安全會有怎樣的安全隱患,具體表現是什麼。緩存
6.爲何Map桶中個數超過8才轉爲紅黑樹?答案:http://cmsblogs.com/?p=4374,簡單就是泊松分佈,達到8的這個機率很是低。安全
7.HashMap爲何要設置初始容量,初始容量設置爲多少比較合適?數據結構
答:默認狀況下,HashMap的容量16,但在實際狀況中,建議咱們在構造函數中設置HashMap的初始容量,主要是爲了防止擴容而引發的性能降低,尤爲是當hashmap中個數不少的時候,擴容是很是花時間的。多線程
在已知須要存儲的個數之後,initialCapacity=(須要存儲的元素個數/負載因子)+1.而後在構造函數源碼中會再轉換成2^n次方。併發
https://www.hollischuang.com/archives/2431分佈式
8.32位的hashmap,若是存儲的元素都集中在後面16位,源碼中是如何解決的?未解決
9.hashmap和數組誰的查詢快?首先hashmap在插入和刪除必定更快,其次,僅僅按下標,數組快,若是按鍵來查,hashmap快,能夠很快映射到哪個快。
10.hashset和hashmap誰的查詢效率更好?
JDK1.8以前:
JDK1.8 以前 HashMap 底層數據結構是 數組和鏈表 結合在一塊兒使用也就是 鏈表散列。HashMap 經過 key 的 hashCode 通過擾動函數處理事後獲得 hash 值,而後經過 (n - 1) & hash
判斷當前元素存放的位置(這裏的 n 指的是數組的長度),若是當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,若是相同的話,直接覆蓋,不相同就經過拉鍊法解決衝突。
JDK1.8 在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減小搜索時間。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 序列號 private static final long serialVersionUID = 362498820763181265L; // 默認的初始容量是16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默認的填充因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 當桶(bucket)上的結點數大於這個值時會轉成紅黑樹 static final int TREEIFY_THRESHOLD = 8; // 當桶(bucket)上的結點數小於這個值時樹轉鏈表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中結構轉化爲紅黑樹對應的table的最小大小 static final int MIN_TREEIFY_CAPACITY = 64; // 存儲元素的數組,老是2的冪次倍 transient Node<k,v>[] table; // 存放具體元素的集 transient Set<map.entry<k,v>> entrySet; // 存放元素的個數,注意這個不等於數組的長度。 transient int size; // 每次擴容和更改map結構的計數器 transient int modCount; // 臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴容 int threshold; // 加載因子 final float loadFactor; }
putMapEntries:能夠插入一個集合
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size();//其中元素的個數 if (s > 0) { // 判斷table是否已經初始化 if (table == null) { // pre-size // 未初始化,s爲m的實際元素個數 float ft = ((float)s / loadFactor) + 1.0F;//計算容量 int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); // 計算獲得的t大於閾值,則初始化閾值 if (t > threshold) threshold = tableSizeFor(t);//這個主要是爲了保證hashmap的容量老是2^n次方 } // 已初始化,而且m元素個數大於閾值,進行擴容處理 else if (s > threshold) resize(); // 將m中的全部元素添加至HashMap中 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict);//把每個都插入進去 } } }
具體的過程:
①若是定位到的數組位置沒有元素 就直接插入。
②若是定位到的數組位置有元素就和要插入的key比較,若是key相同就直接覆蓋,若是key不相同,就判斷p是不是一個樹節點,若是是就調用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)
將元素添加進入。若是不是就遍歷鏈表插入。
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,進行擴容,並保存默認的長度 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
// (n - 1) & hash 肯定元素存放在哪一個桶中,桶爲空,新生成結點放入桶中(此時,這個結點是放在數組中) 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; } // 出現了是同一個節點的狀況,這裏的e其實已經保存了這個節點了,不用再次保存,若是出現直接break if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循環 break; // 用於遍歷桶中的鏈表,與前面的e = p.next組合,能夠遍歷鏈表 p = e; } } // 不爲null說明這個hashmap裏面出現了不是同一個對象的值,就直接覆蓋值就行了,若是爲null,在上面的操做中已經加進去了 if (e != null) { // 記錄e的value V oldValue = e.value; // onlyIfAbsent爲false或者舊值爲null if (!onlyIfAbsent || oldValue == null) //用新值替換舊值 e.value = value; // 訪問後回調 afterNodeAccess(e); // 返回舊值 return oldValue; } } // 結構性修改 ++modCount; // 實際大小大於閾值則擴容 if (++size > threshold) resize(); // 插入後回調 afterNodeInsertion(evict); return null; }
putTreeVal:(不一樣的hash值是能夠映射到同一個桶裏),記住紅黑樹結構這裏還維持了一個雙向鏈表。因此在插入的時候還作了雙向鏈表的操做。
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) { Class<?> kc = null; // 定義k的Class對象 boolean searched = false; // 標識是否已經遍歷過一次樹,未必是從根節點遍歷的,可是遍歷路徑上必定已經包含了後續須要比對的全部節點。 TreeNode<K,V> root = (parent != null) ? root() : this; // 父節點不爲空那麼查找根節點,爲空那麼自身就是根節點 for (TreeNode<K,V> p = root;;) { // 從根節點開始遍歷,沒有終止條件,只能從內部退出 int dir, ph; K pk; // 聲明方向、當前節點hash值、當前節點的鍵對象 if ((ph = p.hash) > h) // 若是當前節點hash 大於 指定key的hash值 dir = -1; // 要添加的元素應該放置在當前節點的左側 else if (ph < h) // 若是當前節點hash 小於 指定key的hash值 dir = 1; // 要添加的元素應該放置在當前節點的右側 else if ((pk = p.key) == k || (k != null && k.equals(pk))) //必定是同一個對象那麼返回,指向的是同一個對象 return p;//到這裏說明hash值相等,那麼既不是指向同一個對象,也沒有重寫equals方法使之相等。 else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||//沒有實現comparable接口 (dir = compareComparables(kc, k, pk)) == 0) { //實現了這個接口,而且比較的時候還相等,只是內容相等,其實並非同一個對象,這種狀況是容許插入到hashmap裏面的,只是這些內容相同的節點都連在一塊兒,在它下面可能存在是一個對象的節點 if (!searched) { // 若是尚未比對過當前節點的全部子節點 TreeNode<K,V> q, ch; // 定義要返回的節點、和子節點 searched = true; // 標識已經遍歷過一次了 /* * 紅黑樹也是二叉樹,因此只要沿着左右兩側遍歷尋找就能夠了 * 這是個短路運算,若是先從左側就已經找到了,右側就不須要遍歷了 * find 方法內部還會有遞歸調用。參見:find方法解析 */ if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null) || ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null)) return q; // 找到了指定key鍵對應的 } //仍然沒有找到相等的節點,那麼就進行最後一道比較
//先比較兩個對象的類名,類名是字符串對象,就按字符串的比較規則
//若是兩個對象是同一個類型,那麼調用本地方法爲兩個對象生成hashCode值,再進行比較,hashCode相等的話返回-1,因此最後一個函數不管能不能比較出結果都要肯定方向
dir = tieBreakOrder(k, pk);
} TreeNode<K,V> xp = p; // 定義xp指向當前節點 /* * 若是dir小於等於0,那麼看當前節點的左節點是否爲空,若是爲空,就能夠把要添加的元素做爲當前節點的左節點,若是不爲空,還須要下一輪繼續比較 * 若是dir大於等於0,那麼看當前節點的右節點是否爲空,若是爲空,就能夠把要添加的元素做爲當前節點的右節點,若是不爲空,還須要下一輪繼續比較 * 若是以上兩條當中有一個子節點不爲空,這個if中還作了一件事,那就是把p已經指向了對應的不爲空的子節點,開始下一輪的比較 */ if ((p = (dir <= 0) ? p.left : p.right) == null) { // 若是剛好要添加的方向上的子節點爲空,此時節點p已經指向了這個空的子節點 Node<K,V> xpn = xp.next; // 獲取當前節點的next節點 TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); // 建立一個新的樹節點 if (dir <= 0) xp.left = x; // 左孩子指向到這個新的樹節點 else xp.right = x; // 右孩子指向到這個新的樹節點 xp.next = x; // 鏈表中的next節點指向到這個新的樹節點 x.parent = x.prev = xp; // 這個新的樹節點的父節點、前節點均設置爲 當前的樹節點 if (xpn != null) // 若是原來的next節點不爲空 ((TreeNode<K,V>)xpn).prev = x; // 那麼原來的next節點的前節點指向到新的樹節點 moveRootToFront(tab, balanceInsertion(root, x));// 從新平衡,以及新的根節點置頂 return null; // 返回空,意味着產生了一個新節點 } } }
hashmap中的紅黑樹最早應該是按hash值來排序的,而後實現了conparable接口就按就按接口裏面的方式來比較,比較仍然相等或者根本就沒有實現接口就按對象類名按字符串的大小來比較。
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {//首先找到對應的插槽 // 數組元素相等 if (first.hash == hash && // 第一個節點就是直接返回 ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 桶中不止一個節點 if ((e = first.next) != null) { // 在樹中get if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key);//二插法來查,時間複雜度log2(N) // 在鏈表中get do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e;//從頭日後查 } while ((e = e.next) != null); } } return null; }
public V remove(Object key) { if (key == null) { return removeNullKey(); } int hash = secondaryHash(key.hashCode()); HashMapEntryGac<K, V>[] tab = table; int index = hash & (tab.length - 1); for (HashMapEntryGac<K, V> e = tab[index], prev = null; e != null; prev = e, e = e.next) { if (e.hash == hash && key.equals(e.key)) { if (prev == null) { tab[index] = e.next; } else { prev.next = e.next; } modCount++; size--; //postRemove(e); return e.value; } } return null; }
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { // 超過最大值就再也不擴充了,就只好隨你碰撞去吧 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 沒超過最大值,就擴充爲原來的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 計算新的resize上限 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) { // 把每一個bucket都移動到新的buckets中 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; 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 {
//記住resize這個地方jdk1.7採用的是頭插法,在多線程的狀況下會出現循環指針,因此在jdk1.8裏面咱們採用了尾插法 Node<K,V> loHead = null, loTail = null;//原來的那一段 Node<K,V> hiHead = null, hiTail = null;//須要擴容的那一段 Node<K,V> next; do { next = e.next; // 原索引 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 原索引+oldCap else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 原索引放到bucket裏 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 原索引+oldCap放到bucket裏 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
關於擴容,就是每次容量和閥值都擴充爲兩倍,而後老是爲2^n主要緣由是
1.咱們在對hash值取模的時候是(length-1)&hash這樣的方式,若是不是2^n次方的話會致使分配不隨機的問題,假如length=15,length-1=1110,機會致使末尾爲1的數永遠不能被entry佔領。形成浪費。
2.觀察源碼咱們發現爲2^n時在咱們擴容的時候是很是方便的,咱們只須要比較n-1多一位,也就是1111後面一位就是10000,第五個這個是否是1,是1咱們新的鏈表就能夠插入到entry[16+index]這個位置,不是1的話就能夠插入到原位置。
HashMap,HashTable,ConcurrentHashMap的區別。
HashMap和HashTable的區別:
synchronized
修飾。(若是你要保證線程安全的話就使用 ConcurrentHashMap 吧!);tableSizeFor()
方法保證,下面給出了源代碼)。也就是說 HashMap 老是使用2的冪做爲哈希表的大小,後面會介紹到爲何是2的冪次方。極高併發下HashTable和ConcurrentHashMap哪一個性能更好,爲何,如何實現的。
底層數據結構: JDK1.7的 ConcurrentHashMap 底層採用 分段的數組+鏈表 實現,JDK1.8 採用的數據結構跟HashMap1.8的結構同樣,數組+鏈表/紅黑二叉樹。Hashtable 和 JDK1.8 以前的 HashMap 的底層數據結構相似都是採用 數組+鏈表的形式,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的;
實現線程安全的方式(重要): ① 在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對整個桶數組進行了分割分段(Segment),每一把鎖只鎖容器其中一部分數據,多線程訪問容器裏不一樣數據段的數據,就不會存在鎖競爭,提升併發訪問率。 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,併發控制使用 synchronized 和 CAS 來操做。(JDK1.6之後 對 synchronized鎖作了不少優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的數據結構,可是已經簡化了屬性,只是爲了兼容舊版本;② Hashtable(同一把鎖) :使用 synchronized 來保證線程安全,效率很是低下。當一個線程訪問同步方法時,其餘線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 添加元素,另外一個線程不能使用 put 添加元素,也不能使用 get,競爭會愈來愈激烈效率越低。
HashMap在高併發下若是沒有處理線程安全會有怎樣的安全隱患,具體表現是什麼。
在jdk1.8以前兩個線程同時插入擴容的時候會出現循環鏈的問題,致使get方法永遠執行,這個問題主要是以前擴容的時候採用的頭插法來插入,jdk以後採用尾插法改變了這個問題。
參考:https://blog.csdn.net/zhuqiuhui/article/details/51849692
可是仍是會有多個線程put的時候致使元素丟失的問題,就是兩個元素添加的元素添加到一個同一個bucket裏面而且key相等就可能會致使前一個數據被覆蓋。
1.對於hashmap裏面經常使用的函數要了解:
在entry這個單鏈表上的每個節點能夠有這些函數操做getKey(), getValue(), setValue(V value), equals(Object o)【這個函數要保證key和value都相等】, hashCode()【這個須要去看源碼實現方式】。
https://www.cnblogs.com/skywang12345/p/3310835.html
2.Hashmap默認是無序的。
對於LinkedHashmap,主要是維持了一個雙向隊列,同時它的順序是按照你的插入順序,若是要按插入的順序排列:LinkedHashmap在實際中最大的應用就是LRU算法。
Map<Integer, String> paramMap = new LinkedHashMap <Integer, String>();//在put函數時,若出現和前面相同的鍵,鍵的值改變,順序仍是按以前的,並非插到最後
當你想定製hashmap按照鍵或者值的增大減少來排序:
參考:
https://blog.csdn.net/xifeijian/article/details/46522531
3.WeakHashMap:WeakHashMap 特殊之處在於 WeakHashMap 裏的entry可能會被垃圾回收器自動刪除,也就是說即便你沒有調用remove()或者clear()方法,它的entry也可能會慢慢變少。因此屢次調用好比isEmpty,containsKey,size等方法時可能會返回不一樣的結果。
一、WeakHashMap中的Entry爲何會自動被回收。
WeakHashMap中的key是間接保存在弱引用中的,因此當key沒有被繼續使用時,就可能會在GC的時候被回收掉。
二、WeakHashMap與HashMap的區別是什麼。
在JDK8中,當發生較多key衝突的時候,HashMap中會由鏈表轉爲紅黑樹,而WeakHashMap則一直使用鏈表進行存儲。WeakHashMap多了一個ReferenceQueue的隊列,用來存放那些已經被回收了的弱引用對象。
三、WeakHashMap的引用場景有哪些。
因爲WeakHashMap能夠自動清除Entry,因此比較適合用於存儲非必需對象,用做緩存很是合適。
4.用hashmap替代redis會有哪些問題?
1,redis 數據可持久化保存,有些緩存你想重啓程序後還能繼續使用,map實現不了
2,redis 能夠實現分佈式部署,只要涉及到多臺多進程啥的,map實現不了
3,hashmap不是線程安全的(而且:多線程同時調用hashMap的resize方法後,後續調用get方法時,可能進入死循環),能夠考慮concurrentHashmap
爲何要用redis而不用map作緩存?
Redis 能夠用幾十 G 內存來作緩存,Map 不行,通常 JVM 也就分幾個 G 數據就夠大了
Redis 的緩存能夠持久化,Map 是內存對象,程序一重啓數據就沒了
Redis 能夠實現分佈式的緩存,Map 只能存在建立它的程序裏
Redis 緩存有過時機制,Map 自己無此功能
參考: