java面試之常見問題彙總

面試常見問題總結

java面試之【java中的鎖】


  • java中的鎖

    • 公平鎖/非公平鎖

      • 公平鎖是指多個線程按照申請鎖的順序來獲取鎖。 非公平鎖是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖,就有可能造成優先級反轉或者飢餓現象。 ReentrantLock:通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。 Synchronized也是一種非公平鎖。由於其並不像ReentrantLock是通過AQS(AbstractQueuedSynchronized)來實現線程調度,所以並沒有任何辦法使其變成公平鎖。

    • 可重入鎖

      • 可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖,可重入鎖的一個好處是可一定程度避免死鎖。

    • 獨享鎖/共享鎖

      • 獨享鎖是指該鎖一次只能被一個線程所持有。 共享鎖是指該鎖可被多個線程所持有。 ReentrantLock是獨享鎖,另一個ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。 讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。 獨享鎖與共享鎖也是通過AQS(AbstractQueuedSynchronized)來實現的,通過實現不同的方法,來實現獨享或者共享。 Synchronized也是獨享鎖。

    • 互斥鎖/讀寫鎖

      • 上面講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。 互斥鎖在Java中的具體實現就是ReentrantLock 讀寫鎖在Java中的具體實現就是ReadWriteLock

    • 樂觀鎖/悲觀鎖

      • 樂觀鎖與悲觀鎖不是指具體的什麼類型的鎖,而是指看待併發同步的角度。 悲觀鎖認爲對於同一個數據的併發操作,一定是會發生修改的,哪怕沒有修改,也會認爲修改。因此對於同一個數據的併發操作,悲觀鎖採取加鎖的形式。悲觀的認爲,不加鎖的併發操作一定會出問題。 樂觀鎖則認爲對於同一個數據的併發操作,是不會發生修改的。 在更新數據的時候,會採用嘗試更新,不斷重新的方式更新數據。樂觀的認爲,不加鎖的併發操作是沒有事情的。從上面的描述我們可以看出,悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升。悲觀鎖在Java中的使用,就是利用各種鎖。樂觀鎖在Java中的使用,是無鎖編程。

    • 分段鎖

      • 分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是通過分段鎖的形式來實現高效的併發操作。 分段鎖的含義以及設計思想:ConcurrentHashMap中的分段鎖稱爲Segment,它即類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然後對這個分段進行加鎖,所以當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。但是,在統計size的時候,可就是獲取hashmap全局信息的時候,就需要獲取所有的分段鎖才能統計。 分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。

    • 自旋鎖

      • 在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。

    • 偏向鎖/輕量級鎖/重量級鎖

      • 這三種鎖是指鎖的狀態,並且是針對Synchronized。 偏向鎖:是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。降低獲取鎖的代價。輕量級鎖:是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。 重量級鎖:是指當鎖爲輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其他申請的線程進入阻塞,性能降低。

以上均爲總結的面試經驗,如果哪個地方有問題,歡迎指正。

java面試之【hashmap原理分析】


  • 什麼是HashMap

    • 基於哈希表的 Map 接口的實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。(除了非同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證映射的順序,特別是它不保證該順序恆久不變。 此實現假定哈希函數將元素適當地分佈在各桶之間,可爲基本操作(get 和 put)提供穩定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的“容量”(桶的數量)及其大小(鍵-值映射關係數)成比例。所以,如果迭代性能很重要,則不要將初始容量設置得太高(或將加載因子設置得太低)。

  • HashMap原理分析

    • 數據結構(哈希桶數組)

      • 數組(數組的特點是:尋址容易,插入和刪除困難)+鏈表(鏈表的特點是:尋址困難,插入和刪除容易)+紅黑樹(結合上面兩種存儲結構的優勢)

      • hash 函數

        • 用來獲取數組下標,把數據放在對應下標元素的鏈表上

      • 幾個參數

        • length:Node[]初始化長度 默認爲16

        • loadFactor:負載因子 默認0.75 負載因子越大,所能容納的鍵值對個數越多

        • threshold:所能存儲的最大Node個數(鍵值對)threshold = length * loadFactor (當threshold>length * loadFactor時執行resize擴容,擴容至原來兩倍)

        • HashMap的存取實現

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;    //用來定位數組索引位置
        final K key;
        V value;
        Node<K,V> next;   //鏈表的下一個node
}

當鏈表長度>8時轉換爲紅黑樹

    // 存儲時:
    int hash = key.hashCode(); // 這個hashCode方法這裏不詳述,只要理解每個key的hash是一個固定的int值
    int index = hash % Entry[].length;
    Entry[index] = value;
    // 取值時:
    int hash = key.hashCode();
    int index = hash % Entry[].length;
    return Entry[index];
  • put()

    • 如果兩個key通過hash%Entry[].length得到的index相同,會不會有覆蓋的危險?

      • 這裏HashMap裏面用到鏈式數據結構的一個概念。上面我們提到過Entry類裏面有一個next屬性,作用是指向下一個Entry,也就是說數組中存儲的是最後插入的元素

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value); //null總是放在數組的第一個鏈表中
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        //遍歷鏈表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果key在鏈表中已存在,則替換爲新value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
​
 
​
void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //參數e, 是Entry.next
    //如果size超過threshold,則擴充table大小。再散列
    if (size++ >= threshold)
            resize(2 * table.length);
}
  • get()

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        //先定位到數組元素,再遍歷該元素處的鏈表
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
}
  • null key/value 的存取 null key總是存放在Entry[]數組的第一個元素。

     private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
 
    private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
  • 獲取節點在數組中的位置 HashMap存取時,都需要計算當前key應該對應Entry[]數組哪個元素,即計算數組下標;算法如下:

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
  • hash 衝突解決 開放定址法(線性探測再散列,二次探測再散列,僞隨機探測再散列) 再哈希法 鏈地址法 建立一個公共溢出區

Java中hashmap的解決辦法就是採用的 鏈地址法

java面試之【redis】


  • redis cluster集羣方式

    • Redis Sentinal着眼於高可用,在master宕機時會自動將slave提升爲master,繼續提供服務

    • Redis Cluster着眼於擴展性,在單個redis內存不足時,使用Cluster進行分片存儲

  • Redis有哪些數據結構

    •  字符串String

    • 字典Hash

    • 列表List

    • 集合Set

    • 有序集合SortedSet

    如果你是Redis中高級用戶,還需要加上下面幾種數據結構HyperLogLog、Geo、   Pub/Sub。Redis Module,像BloomFilter,RedisSearch,Redis-ML

  • Redis分佈式鎖

    • 先拿setnx來爭搶鎖,搶到之後,再用expire給鎖加一個過期時間防止鎖忘記了釋放

    • 線上使用keys會發生什麼

      • redis關鍵的一個特性:redis的單線程的。keys指令會導致線程阻塞一段時間,線上服務會停頓,直到指令執行完畢,服務才能恢復,這個時候可以使用scan指令,scan指令可以無阻塞的提取出指定模式的key列表,但是會有一定的重複概率,在客戶端做一次去重就可以了,但是整體所花費的時間會比直接用keys指令長

  • Redis做異步隊列

    • 使用list結構作爲隊列,rpush生產消息,lpop消費消息。

      • 當lpop沒有消息的時候,要適當sleep一會再重試

      •  list還有個指令叫blpop,在沒有消息的時候,它會阻塞住直到消息到來

  • redis如何實現延時隊列

    •  使用sortedset,拿時間戳作爲score,消息內容作爲key調用zadd來生產消息,消費者用zrangebyscore指令獲取N秒之前的數據輪詢進行處理

  • Redis如何做持久化的redis 同步的兩種方式,利弊)

    • bgsave做鏡像全量持久化

      • bgsave 原理

        • fork和cow。fork是指redis通過創建子進程來進行bgsave操作,cow指的是copy on write,子進程創建後,父子進程共享數據段,父進程繼續提供讀寫服務,寫髒的頁面數據會逐漸和子進程分離開來

    • aof做增量持久化

    • 因爲bgsave會耗費較長時間,不夠實時,在停機的時候會導致大量丟失數據,所以需要aof來配合使用

    • 在redis實例重啓時,會使用bgsave持久化文件重新構建內存,再使用aof重放近期的操作指令來實現完整恢復重啓之前的狀態

  • redis aof同步時間

    • 取決於aof日誌sync屬性的配置,如果不要求性能,在每條寫指令時都sync一下磁盤,就不會丟失數據。但是在高性能的要求下每次都sync是不現實的,一般都使用定時sync,比如1s1次,這個時候最多就會丟失1s的數據

  • Redis的同步機制redis集羣同步方式

    • Redis可以使用主從同步,從從同步

      • 第一次同步時,主節點做一次bgsave,並同時將後續修改操作記錄到內存buffer,待完成後將rdb文件全量同步到複製節點,複製節點接受完成後將rdb鏡像加載到內存

      • 加載完成後,再通知主節點將期間修改的操作記錄同步到複製節點進行重放就完成了同步過程

  • redis常見的性能問題及解決方案

    • Master最好不要做任何持久化工作,如RDB內存快照和AOF日誌文件

    • 如果數據比較重要,某個Slave開啓AOF備份數據,策略設置爲每秒同步一次

    • 爲了主從複製的速度和連接的穩定性,Master和Slave最好在同一個局域網內

    •  儘量避免在壓力很大的主庫上增加從庫

    • 主從複製不要用圖狀結構,用單向鏈表結構更爲穩定,即:Master <- Slave1 <- Slave2 <- Slave3…  這樣的結構方便解決單點故障問題,實現Slave對Master的替換。如果Master掛了,可以立刻啓用Slave1做Master,其他不變

java面試之【mysql】


  • 事務的基本要素 1、 原子性(Atomicity):事務開始後所有操作,要麼全部做完,要麼全部不做,不可能停滯在中間環節。事務執行過程中出錯,會回滾到事務開始前的狀態,所有的操作就像沒有發生一樣。也就是說事務是一個不可分割的整體。 2、 一致性(Consistency):事務開始前和結束後,數據庫的完整性約束沒有被破壞 。 3、 隔離性(Isolation):同一時間,只允許一個事務請求同一數據,不同的事務之間彼此沒有任何干擾。 4、 持久性(Durability):事務完成後,事務對數據庫的所有更新將被保存到數據庫,不能回滾。

  • 事務的併發問題 1、 髒讀:事務A讀取了事務B更新的數據,然後B回滾操作,那麼A讀取到的數據是髒數據 2、 不可重複讀:事務 A 多次讀取同一數據,事務 B 在事務A多次讀取的過程中,對數據作了更新並提交,導致事務A多次讀取同一數據時,結果 不一致。 3、 幻讀:系統管理員A將數據庫中所有學生的成績從具體分數改爲ABCDE等級,但是系統管理員B就在這個時候插入了一條具體分數的記錄,當系統管理員A改結束後發現還有一條記錄沒有改過來,就好像發生了幻覺一樣,這就叫幻讀。

  • 數據庫事務隔離級別

事務隔離級別 髒讀 不可重複讀 幻讀
讀未提交(read-uncommitted)
不可重複讀(read-committed)
可重複讀(repeatable-read)
串行化(serializable)
  • 存儲引擎

  • 事物回滾怎麼實現的

java面試之【volatile】


  • volatile

    • JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。但是就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題,所以在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操作的時候,會強制重新從系統內存裏把數據讀到處理器緩存裏。

java面試之【一致性hash】


  • 一致性hash

java 面試之【內存溢出及解決方案】


  • java.lang.OutOfMemoryError: Java heap space -java堆內存不夠

    • 原因

      • 真的是堆內存不夠

      • 程序中有死循環

    • 方案

      • -Xms 調整堆的最小值

      • -Xmx 調整堆的最大值

  • java.lang.OutOfMemoryError: GC overhead limit exceeded -當GC爲釋放很小空間佔用大量時間時拋出

    • 原因

      • 堆太小,沒有足夠的內存

    • 方案

      • 查看系統是否有使用大內存的代碼或死循環

      • 通過添加JVM配置,來限制使用內存 < jvm-arg>-XX:-UseGCOverheadLimit< /jvm-arg>

  • java.lang.OutOfMemoryError: PermGen space

    • 原因

      • Perm區內存不夠

    • 方案

      • < jvm-arg>-XX:MaxPermSize=128m< /jvm-arg>

        < jvm-arg>-XXermSize=128m< /jvm-arg>

      JVM的Perm區主要用於存放Class和Meta信息的,Class在被Loader時就會被放到PermGen space,這個區域成爲年老代,GC在主程序運行期間不會對年老區進行清理,默認是64M大小,當程序需要加載的對象比較多時,超過64M就會報這部分內存溢出了,需要加大內存分配,一般128m足夠。

  • java.lang.StackOverflowError - 棧內存溢出

    • 原因

      • 方法調用層次過多(比如存在無限遞歸調用)

      • 線程棧太小

    • 方案

      • 優化程序設計,減少方法調用層次

      • 調整-Xss參數增加線程棧大小

  • java.lang.OutOfMemoryError: unable to create new native thread

    • 原因

      • Stack空間不足以創建額外的線程

      • 創建的線程過多

      • Stack空間確實小了

    • 方案

      • 通過 -Xss啓動參數減少單個線程棧大小,這樣便能開更多線程(當然不能太小,太小會出現StackOverflowError)

      • 通過-Xms -Xmx 兩參數減少Heap大小,將內存讓給Stack(前提是保證Heap空間夠用)。

      由於JVM沒有提供參數設置總的stack空間大小,但可以設置單個線程棧的大小;而系統的用戶空間一共是3G,除了Text/Data/BSS /MemoryMapping幾個段之外,Heap和Stack空間的總量有限,是此消彼長的

文章不定期更新,大家如果有好的提議歡迎

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