1. 程式人生 > >java面試之常見問題彙總

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空間的總量有限,是此消彼長的

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