面試(一)-HashMap

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;
}
View Code

      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;
    }
View Code

      

     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;
        }
    }
View Code

       初始化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;
    }
View Code

     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;
            }
        }
}
View Code

     以上爲主要代碼,接下來咱們來談下照成死循環的過程,下面這段代碼是主要的罪魁禍首:

     

      首先來看下單線程下運行情況,代碼以下,過程如圖:

      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");
View Code

      

        思考一個問題,當多線程操做作時候,這塊不是原子操做,因此確定會出現問題,假設這樣一種狀況,代碼以下:

        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");
View Code

       當線程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)));
            }
        }
View Code

       

 3、結束語

這幾個問題要好好深耕一下的話,估計1篇或者幾篇是寫不完的,我會慢慢寫,請你們耐心等待吧,先進行一個預告,下一篇我會再把第一題發散一下:HashMap和ConcurrentHashMap對比,上面有不明白的地方能夠找我,QQ羣:438836709

相關文章
相關標籤/搜索