1、前言html
其實這一面來的挺忽然,也是意想不到的,這個要起源於BOSS直聘,很巧,其實也算是一種緣分吧,謝謝BOSS那個哥們,仍是那句話來濱江我請你吃飯,身懷感激你總會遇到幫助你的人,只是這個我沒有想到,我轉Java也沒多久,不少東西也沒有搞清楚,沒想到菜鳥會給我這個電話,心裏是震驚的,可是也感謝給我這個機會讓我真正認清本身,謝謝菜鳥,那我如今將這份遲到答卷交一交吧。java
2、試卷面試
一、HashMap和Hashtable區別在哪? 算法
二、Synchronized是怎麼實現同步的?數組
三、Spring IOC的啓動流程?另外還說一下BeanFactory和ApplicationContext的區別?安全
四、攔截器和過濾器的區別?數據結構
五、單點登陸?多線程
六、RabbitMQ怎麼保證消息到達的?app
這6個問題其實我感受就第5個問題回答的比較好,剩下都是似懂非懂,這多是對於個人自滿的一個警鐘,仍是那句話深挖基礎,來年再戰,接下來咱們來用來深耕一下這些問題,給本身再交一份滿意的答卷。ide
第一個問題咱們從基層結構、對null值的處理、數據結構以及線程安全這4個方面結合源碼來談一下:
1.繼承結構
Map整體繼承:
HashMap的繼承結構:
HashTable的繼承結構:
對比下實現,不一樣點主要在基類上繼承上,對比下暴露出的API主要是Hashtable多elements和contains方法,elemets主要是返回全部的value的值,爲空的時候返回空枚舉對象,而contains方法和containsValue方法都是查詢是否包含value的值,剩下方法基本上都是相同。
2.對null的處理
Hashtable對null的處理,若是發現爲null會報空引用異常;
public synchronized V put(K key, V value) { // value爲空引用異常 if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry<?,?> tab[] = table; //key計算的時候一樣爲空引用異常 int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } addEntry(hash, key, value, index); return null; }
HashMap對Null的處理以及添加元素的整個流程:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h; //key爲null則默認hashcode爲0 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //判斷一下是否爲第一次插入,若是是第一次插入則建立 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //根據鍵值key計算hash值獲得插入的數組索引i,若是table[i]==null,直接新建節點添加 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else {//對應的節點存在 Node<K,V> e; K k; //判斷table[i]的首個元素是否和key同樣,若是相同直接覆蓋value if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //判斷table[i] 是否爲treeNode,即table[i] 是不是紅黑樹,若是是紅黑樹,則直接在樹中插入鍵值對 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); //若是個數大於8則轉爲紅黑樹 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; } } //找到或者新建key和hashcode相等的鍵值進行插入 if (e != null) { // existing mapping for key V oldValue = e.value; //onlyIfAbsent爲false或舊值爲null時,容許替換舊值 //這個地方有點不明,明明啥時候都爲false if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
3.數據結構
整體上二者的數據結構都是經過數組+鏈表實現,可是JDK1.8之後當鏈表的長度超過8之後會更改成紅黑樹,都是經過繼承Entry數組實現哈希表;
//Hashtable節點定義 private static class Entry<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Entry<K,V> next; protected Entry(int hash, K key, V value, Entry<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } @SuppressWarnings("unchecked") protected Object clone() { return new Entry<>(hash, key, value, (next==null ? null : (Entry<K,V>) next.clone())); } // Map.Entry Ops public K getKey() { return key; } public V getValue() { return value; } public V setValue(V value) { if (value == null) throw new NullPointerException(); V oldValue = this.value; this.value = value; return oldValue; } public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>)o; return (key==null ? e.getKey()==null : key.equals(e.getKey())) && (value==null ? e.getValue()==null : value.equals(e.getValue())); } public int hashCode() { return hash ^ Objects.hashCode(value); } public String toString() { return key.toString()+"="+value.toString(); } } //HashMap節點定義 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
初始化Hashtable默認初始化的長度爲11,加載因子爲0.75;HashMap默認爲16,加載因子爲0.75
擴容Hashtable爲2n+1,HashMap爲2倍,另外HashMap若是爲整數的最大值則就不會擴容,這塊是JDK8實現的別的還請本身去查看一下;
另外就是算法問題,JDK8中實現高位運算的算法,相比Hashtable的去模算法來具備更高的效率;具體看下https://tech.meituan.com/java-hashmap.html,
擴容算法也有必要看下;
//Hashtable默認構造函數 public Hashtable() { this(11, 0.75f); } //Hashtable擴容 protected void rehash() { int oldCapacity = table.length; Entry<?,?>[] oldMap = table; // overflow-conscious code //位運算至關於2n+1 int newCapacity = (oldCapacity << 1) + 1; if (newCapacity - MAX_ARRAY_SIZE > 0) { if (oldCapacity == MAX_ARRAY_SIZE) // Keep running with MAX_ARRAY_SIZE buckets return; newCapacity = MAX_ARRAY_SIZE; } Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; modCount++; threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); table = newMap; for (int i = oldCapacity ; i-- > 0 ;) { for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) { Entry<K,V> e = old; old = old.next; int index = (e.hash & 0x7FFFFFFF) % newCapacity; e.next = (Entry<K,V>)newMap[index]; newMap[index] = e; } } } //HashMap擴容 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; //初始化默認爲16 else { // zero initial threshold 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) { 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 { // preserve order 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; } 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; }
4.線程安全性
主要針對這塊說一說,我就是死在這裏的,首先明確的是Hashtable確定是線程安全的,由於使用Synchronized修飾,因此內部調用的時候是線程安全的;接下主要說下HashMap,這個其實面試官感受我能回答上來,還好幾回問我你肯定嘛,我說我肯定,哈哈當時也真是,我沒說出死循環這個點來,接下來咱們分析下爲何會照成死循環?一樣上面那個美團技術博客也是有這部分講解的,這部分JDK8確實寫的很麻煩,咱們使用JDK7來看下怎麼照成的,站到巨人的肩膀上,消化爲本身的;
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); } 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; 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.擴容的時候首先遍歷數組中的元素;
2.對鏈表的每個節點進行遍歷,next指向下一個要移動元素,將e轉向轉向Hash表的頭部,使用頭插入元素;
3.循環到鏈表爲null結束此次循環;
4.最終等數組循環完畢,完成HashMap擴容;
HashMap<Integer,String> map = new HashMap<>(2,0.75f); map.put(5, "C"); map.put(7, "B"); map.put(3, "A");
思考一個問題,當多線程操做作時候,這塊不是原子操做,因此確定會出現問題,假設這樣一種狀況,代碼以下:
HashMap<Integer,String> map = new HashMap<>(2,0.75f); map.put(5, "C"); new Thread("Thread1") { public void run() { map.put(7, "B"); System.out.println(map); }; }.start(); new Thread("Thread2") { public void run() { map.put(3, "A"); System.out.println(map); }; }.start(); map.put(11,"D");
當線程1和線程2同時進入put方法,進入transfer()該方法時候,線程1執行到"Entry<k,v> next = e.next"而後被掛起,線程2開始執行resize()方法,最終造成以下:這部分可使用多線程調試進行模擬,不會的請參考:http://blog.csdn.net/kevindai007/article/details/71412324
這個時候線程1被喚醒,執行步驟以下:
1.執行e.next = newTable[i],這個時候 key(3)的 next 指向了線程1的新 Hash 表,因此e.next=null;
2.執行newTable[i]=e,e = next,致使e指向了key(7);
3.下一次循環的next = e.next致使了next指向了key(3)。
4.e.next = newTable[i] 致使 key(3).next 指向了 key(7),環形鏈表就這樣出現了。
另外還有一種狀況,也就是常見的更新丟失問題的,當2個線程同時插入到數組同一個位置的時候,線程A也寫入,線程B也寫入,則會照成B寫入的覆蓋A寫入的,照成更新丟失,執行以下代碼,這種狀況不是很容易出現,多執行幾回代碼會發現,截圖以下:
HashMap<String,String> map=new HashMap<>(); //線程1 Thread t1 = new Thread(){ public void run() { for(int i=0; i<25; i++){ map.put(String.valueOf(i), String.valueOf(i)); } } }; //線程2 Thread t2 = new Thread(){ public void run() { for(int i=25; i<50; i++){ map.put(String.valueOf(i), String.valueOf(i)); } } }; t1.start(); t2.start(); Thread.currentThread().sleep(1000); //System.out.print(map.values()); for(int i=0; i<50; i++){ //若是key和value不一樣,說明在兩個線程put的過程當中出現異常。 if(!String.valueOf(i).equals(map.get(String.valueOf(i)))){ System.err.println(String.valueOf(i) + ":" + map.get(String.valueOf(i))); } }
3、結束語
這幾個問題要好好深耕一下的話,估計1篇或者幾篇是寫不完的,我會慢慢寫,請你們耐心等待吧,先進行一個預告,下一篇我會再把第一題發散一下:HashMap和ConcurrentHashMap對比,上面有不明白的地方能夠找我,QQ羣:438836709