1. 程式人生 > 其它 >【刷題】牛客模擬面試 > 模擬面試報告

【刷題】牛客模擬面試 > 模擬面試報告

https://www.nowcoder.com/interview/ai/index

1-TCP協議的流量控制和擁塞控制 

TCP的流量控制是基於視窗機制實現的: 在建立連線時, 傳送方和接收方都會建立一個快取區,在兩端進行通訊時,資料包頭部會有一個視窗欄位,標識了接收端剩餘的快取空間。傳送方根據視窗欄位的值去判斷髮送資料的大小,從而避免了快取溢位。 TCP的擁塞控制演算法包含了: 慢啟動,擁塞避免,快速重傳,快速恢復 慢啟動指的是傳送資料的量從較低的起始值,如一個報文段慢慢指數增長 擁塞避免是指當擁塞的視窗小於閾值時,又指數增長降低為線性增長 快速重傳是指超過三次重複確認即視為傳輸失敗,立即重傳 快速恢復是指發生快速重傳後,立刻減低視窗閾值,並進行擁塞避免的線性增長演算法,避免因為擁塞阻礙了重傳

2-說一說ConcurrentHashMap的實現原理

參考回答

    陣列+連結串列+紅黑樹、鎖頭節點

標準回答

    在JDK8中,ConcurrentHashMap的底層資料結構與HashMap一樣,也是採用“陣列+連結串列+紅黑樹”的形式。同時,它又採用鎖定頭節點的方式降低了鎖粒度,以較低的效能代價實現了執行緒安全。底層資料結構的邏輯可以參考HashMap的實現,下面我重點介紹它的執行緒安全的實現機制。

1. 初始化陣列或頭節點時,ConcurrentHashMap並沒有加鎖,而是CAS的方式進行原子替換(原子操作,基於Unsafe類的原子操作API)。

2. 插入資料時會進行加鎖處理,但鎖定的不是整個陣列,而是槽中的頭節點。所以,ConcurrentHashMap中鎖的粒度是槽,而不是整個陣列,併發的效能很好。

3. 擴容時會進行加鎖處理,鎖定的仍然是頭節點。並且,支援多個執行緒同時對陣列擴容,提高併發能力。每個執行緒需先以CAS操作搶任務,爭搶一段連續槽位的資料轉移權。搶到任務後,該執行緒會鎖定槽內的頭節點,然後將連結串列或樹中的資料遷移到新的數組裡。

4. 查詢資料時並不會加鎖,所以效能很好。另外,在擴容的過程中,依然可以支援查詢操作。如果某個槽還未進行遷移,則直接可以從舊數組裡找到資料。如果某個槽已經遷移完畢,但是整個擴容還沒結束,則擴容執行緒會建立一個轉發節點存入舊陣列,屆時查詢執行緒根據轉發節點的提示,從新陣列中找到目標資料。

加分回答

    ConcurrentHashMap實現執行緒安全的難點在於多執行緒併發擴容,即當一個執行緒在插入資料時,若發現數組正在擴容,那麼它就會立即參與擴容操作,完成擴容後再插入資料到新陣列。在擴容的時候,多個執行緒共同分擔資料遷移任務,每個執行緒負責的遷移數量是 (陣列長度 >>> 3) / CPU核心數。

    也就是說,為執行緒分配的遷移任務,是充分考慮了硬體的處理能力的。多個執行緒依據硬體的處理能力,平均分攤一部分槽的遷移工作。另外,如果計算出來的遷移數量小於16,則強制將其改為16,這是考慮到目前伺服器領域主流的CPU執行速度,每次處理的任務過少,對於CPU的算力也是一種浪費。

3-介紹一下分代回收機制

【得分點】

    新生代收集、老年代收集、混合收集、整堆收集

標準回答

    當前商業虛擬機器的垃圾收集器,大多數都遵循了“分代收集”的理論進行設計,分代收集名為理論,實質是一套符合大多數程式執行實際情況的經驗法則。而分代收集理論,建立在如下三個分代假說之上,即弱分代假說、強分代假說、跨代引用假說。依據分代假說理論,垃圾回收可以分為如下幾類:

1. 新生代收集:目標為新生代的垃圾收集。

2. 老年代收集:目標為老年代的垃圾收集,目前只有CMS收集器會有這種行為。

3. 混合收集:目標為整個新生代及部分老年代的垃圾收集,目前只有G1收集器會有這種行為。

4. 整堆收集:目標為整個堆和方法區的垃圾收集。

加分回答

    HotSpot虛擬機器內建了很多垃圾收集器,其中針對新生代的垃圾收集器有Serial、ParNew、Parallel Scavenge,針對老年代的垃圾收集器有CMS、Serial Old、Parallel Old。此外,HotSpot還內建了面向整堆的G1收集器。在上述收集器中,常見的組合方式有:

1. Serial + Serial Old,是客戶端模式下常用的收集器。

2. ParNew + CMS,是服務端模式下常用的收集器。

3. Parallel Scavenge + Parallel Old,適用於後臺運算而不需要太多互動的分析任務。

【延伸閱讀】

    三個分代假說:

1. 弱分代假說:絕大多數物件都是朝生夕滅的。

2. 強分代假說:熬過越多次垃圾收集過程的物件越難以消亡。

3. 跨代引用假說:跨代引用相對於同代引用來說只佔極少數。

    前兩條假說奠定了多款常用垃圾收集器的一致的設計原則:收集器應該將Java堆劃分出不同的區域,然後將回收物件依據其年齡分配到不同的區域之中儲存。根據這兩條假說,設計者一般至少會把Java堆劃分為新生代和老年代兩個區域。在新生代中,每次垃圾收集時都發現有大批物件死去,而每次回收後存活的少量物件,將會逐步晉升到老年代中存放。

    第三條假說是根據前兩條假說推理得出的隱含結論:存在互相引用關係的兩個物件,是應該傾向於同時生存或者同時消亡的。依據這條假說,我們就不應再為了少量的跨代引用去掃描整個老年代,也不必浪費空間專門記錄每一個物件是否存在及存在哪些跨代引用,只需在新生代上建立一個全域性的資料結構,這個結構把老年代劃分成若干小塊,標識出老年代的哪一塊記憶體會存在跨代引用。

4-說一說你對Spring IoC的理解

spring ioc是spring兩大核心之一,spring為我們提供了一個ioc容器,也就是beanFactory,同時,ioc有個非常強大的功能,叫做di,也就是依賴注入,我們可以通過配置或者xml檔案的方式將bean所依賴的物件通過name(名字)或者type(類別)注入進這個beanFactory中,正因為這個依賴注入,實現類與依賴類之間的解耦,如果在一個複雜的系統中,類之間的依賴關係特別複雜,首先,這非常不利於後期程式碼的維護,ioc就很好的幫助我們解決了這個問題,它幫助我們維護了類與類之間的依賴關係,降低了耦合性,使我們的類不需要強依賴於某個類,而且,在spring容器啟動的時候,spring容器會幫助我們自動的建立好所有的bean,這樣,我們程式執行的過程中就不需要花費時間去建立這些bean,速度就快了許多。

標準回答

IoC是控制反轉的意思,是一種面向物件程式設計的設計思想。在不採用這種思想的情況下,我們需要自己維護物件與物件之間的依賴關係,很容易造成物件之間的耦合度過高。尤其是在一個大型的專案中,物件與物件之間的關係是十分複雜的,這十分不利於程式碼的維護。IoC則可以解決這種問題,它可以幫我們維護物件與物件之間的依賴關係,並且降低物件之間的耦合度。

說到IoC就不得不說DI,DI是依賴注入的意思,它是IoC實現的實現方式。由於IoC這個詞彙比較抽象而DI比較直觀,所以很多時候我們就用DI來代替它,在很多時候我們簡單地將IoC和DI劃等號,這是一種習慣。實現依賴注入的關鍵是IoC容器,它的本質就是一個工廠。

加分回答

在以Spring為代表的輕量級Java EE開發風行之前,實際開發中是使用更多的是EJB為代表的開發模式。在EJB開發模式中,開發人員需要編寫EJB元件,這種元件需要滿足EJB規範才能在EJB容器中執行,從而完成獲取事務,生命週期管理等基本服務。

Spring提供的服務和EJB並沒有什麼區別,只是在具體怎樣獲取服務的方式上兩者的設計有很大不同:Spring IoC提供了一個基本的JavaBean容器,通過IoC模式管理依賴關係,並通過依賴注入和AOP切面增強了為JavaBean服務於事務管理、生命週期管理等基本功能。

而對於EJB,一個簡單的EJB元件需要編寫遠端/本地介面、Home介面和Bean的實體類,而且EJB執行不能脫離EJB容器,查詢其他EJB元件也需要通過諸如JNDI的方式,這就造成了對EJB容器和技術規範的依賴。也就是說Spring把EJB元件還原成了POJO物件或者JavaBean物件,以此降低了用用開發對於傳統J2EE技術規範的依賴。

在應用開發中開發人員設計元件時往往需要引用和呼叫其他元件的服務,這種依賴關係如果固化在元件設計中,會造成依賴關係的僵化和維護難度的增加,這個時候使用IoC把資源獲取的方向反轉,讓IoC容器主動管理這些依賴關係,將這些依賴關係注入到元件中,這就會讓這些依賴關係的適配和管理更加靈活。

延伸閱讀

Spring主要提供了兩種型別的容器:BeanFactory和ApplicationContext。

  • BeanFactory:是基礎型別的IoC容器,提供完整的IoC服務支援。如果沒有特殊指定,預設採用延
    遲初始化策略。只有當客戶端物件需要訪問容器中的某個受管物件的時候,才對該受管物件進行初始化以及依賴注入操作。所以,相對來說,容器啟動初期速度較快,所需要的資源有限。對於資源有限,並且功能要求不是很嚴格的場景,BeanFactory是比較合適的IoC容器選擇。
  • ApplicationContext:它是在BeanFactory的基礎上構建的,是相對比較高階的容器實現,除了擁有BeanFactory的所有支援,ApplicationContext還提供了其他高階特性,比如事件釋出、國際化資訊支援等。ApplicationContext所管理的物件,在該型別容器啟動之後,預設全部初始化並繫結完成。所以,相對於BeanFactory來說,ApplicationContext要求更多的系統資源,同時,因為在啟動時就完成所有初始化,容
    器啟動時間較之BeanFactory也會長一些。在那些系統資源充足,並且要求更多功能的場景中,ApplicationContext型別的容器是比較合適的選擇。

在具體的實現中,主要有三種注入方式:

  1. 構造方法注入

    就是被注入物件可以在它的構造方法中宣告依賴物件的引數列表,讓外部知道它需要哪些依賴物件。然後,IoC Service Provider會檢查被注入的物件的構造方法,取得它所需要的依賴物件列表,進而為其注入相應的物件。構造方法注入方式比較直觀,物件被構造完成後,即進入就緒狀態,可以馬上使用。

  2. setter方法注入

    通過setter方法,可以更改相應的物件屬性。所以,當前物件只要為其依賴物件所對應的屬性新增setter方法,就可以通過setter方法將相應的依賴物件設定到被注入物件中。setter方法注入雖不像構造方法注入那樣,讓物件構造完成後即可使用,但相對來說更寬鬆一些,可以在物件構造完成後再注入。

  3. 介面注入

    相對於前兩種注入方式來說,介面注入沒有那麼簡單明瞭。被注入物件如果想要IoC Service Provider為其注入依賴物件,就必須實現某個介面。這個介面提供一個方法,用來為其注入依賴物件。IoC Service Provider最終通過這些介面來了解應該為被注入物件注入什麼依賴物件。相對於前兩種依賴注入方式,介面注入比較死板和繁瑣。

總體來說,構造方法注入和setter方法注入因為其侵入性較低,且易於理解和使用,所以是現在使用最多的注入方式。而介面注入因為侵入性較強,近年來已經不流行了。

5-從上到下按層列印二叉樹,同一層結點從左至右輸出。每一層輸出一行。

給定一個節點數為 n 二叉樹,要求從上到下按層列印二叉樹的 val 值,同一層結點從左至右輸出,每一層輸出一行,將輸出的結果存放到一個二維陣列中返回。 例如:
給定的二叉樹是{1,2,3,#,#,4,5} 該二叉樹多行列印層序遍歷的結果是 [ [1], [2,3], [4,5] ] 資料範圍:二叉樹的節點數 , 要求:空間複雜度 ,時間複雜度 
/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
 
    }
 
}
*/
public class Solution {
    ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> result = new ArrayList<ArrayList<Integer>>();
        if(pRoot==null) return result;
        ArrayList<TreeNode> queue = new ArrayList<TreeNode>();
        ArrayList<Integer> temp = new ArrayList<Integer>();
        ArrayList<Integer> start = new ArrayList<Integer>();
        start.add(pRoot.val);
        result.add(start);
        int low = 0;
        int high = 1;
        int end = high;    
        queue.add(pRoot);
        while(low<high){            
            TreeNode t = queue.get(low);
            if(t.left!=null){
                queue.add(t.left);
                temp.add(t.left.val);
                high++;
            }
            if(t.right!=null){
                queue.add(t.right);
                temp.add(t.right.val);
                high++;
            }
            low++;
            if(low==end){
                end = high;
                if(temp.size()!=0)
                    result.add(temp);
                temp = new ArrayList<Integer>();
            }
        }
        return result;
    }
     
}

round-2

1-談談InnoDB引擎中的鎖

兩種思想:樂觀鎖 和悲觀鎖兩種 樂觀鎖:假設在併發操作中不發生衝突,因此在訪問、處理資料過程中不加鎖,只在更新資料時再根據版本號或時間戳判斷是否有衝突,有則處理,無則提交事務 悲觀鎖:假設在併發操作中大概率發生衝突,因此訪問、處理資料前就加排它鎖,在整個資料處理過程中鎖定資料,事務提交或回滾後才釋放鎖 兩種上鎖方式:排它鎖和共享鎖 共享鎖:允許獲取共享鎖的事務獲取資料(多個事務可以同時獲得共享鎖) 排它鎖:允許獲取排他鎖的事務獲取更改資料(只允許一個事務獲得排他鎖) innodb支援行級鎖,粒度最小,衝突發生的概率極低,支援併發操作的程度最高

悲觀鎖-Pessimistic lock
全域性鎖:flush table with read lock;使用全域性鎖會鎖住整個資料庫,使其處於只讀狀態;
表鎖: lock table和 意向鎖(Intention Locks) MataData Lock ,意向鎖不用顯示呼叫;
行鎖(Record-lock)
間隙鎖( gap locks)
臨鍵鎖( next-key lock) ,由行鎖和間隙鎖組成;
樂觀鎖- Optimistic Lock
自旋cas機制;可通過version版本和時間戳來判斷;

Mysql的行鎖和表鎖區別:
表級鎖: 鎖住整張表。 開銷小,加鎖快;不會出現死鎖;鎖粒度最大,發生鎖衝突的概率最高,併發度低;
行級鎖: 鎖住一行資料。開銷大,加鎖慢;容易出現死鎖;鎖粒度最小,發生鎖衝突的概率最低,併發度高;
行鎖的實現:
如果有索引,那麼會先掃描索引檔案,查詢到主鍵id,通過索引鎖定行記錄實現行鎖;
如果沒有索引,就會鎖住全表的資料;
————————————————

2-建立執行緒有哪幾種方式

一、繼承Thread類建立執行緒類 (1)定義Thread類的子類,並重寫該類的run方法,該run方法的方法體就代表了執行緒要完成的任務。因此把run()方法稱為執行體。 (2)建立Thread子類的例項,即建立了執行緒物件。 (3)呼叫執行緒物件的start()方法來啟動該執行緒。 二、通過Runnable介面建立執行緒類 (1)定義runnable介面的實現類,並重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體。 (2)建立 Runnable實現類的例項,並依此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。 (3)呼叫執行緒物件的start()方法來啟動該執行緒。 三、通過Callable和Future建立執行緒 (1)建立Callable介面的實現類,並實現call()方法,該call()方法將作為執行緒執行體,並且有返回值。 (2)建立Callable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call()方法的返回值。 (3)使用FutureTask物件作為Thread物件的target建立並啟動新執行緒。 (4)呼叫FutureTask物件的get()方法來獲得子執行緒執行結束後的返回值

3-介紹一下分代回收機制

全稱:垃圾分代回收機制。JVM記憶體(執行時資料區)劃分了5個區域,分別是:

1.棧:存放一個個對應方法的棧幀

2.堆:儲存的是容器和物件

3.程式計數器(暫存器):當前執行緒所執行的位元組碼的行號指示器

4.本地方法棧:為虛擬機器使用到的本地方法服務

5.方法區:儲存類資訊,常量,靜態常量以及編譯器編譯後的程式碼等

分代是指堆記憶體又分新生代(Young Generation)和老年代(Old Generation),新生代又分為伊甸區(eden)和倖存區(surivivor),倖存區由from space與to space兩塊相等的記憶體區域組成。eden:from:to = 8:1:1。

發生在新生代的回收:Minor GC

發生在老年代的回收:Major GC

物件建立後會先存放在新生代伊甸區,經過一次回收後移入新生代倖存區;在經過多次(一般為15次,可調整)回收後,移入老年代;如果老年代記憶體不足觸發Major回收後該依然無法放開該物件(物件的儲存),則會報OutOMemryError。

————————————————

4-說一說你對布隆過濾器的理解

布隆過濾器可以用很小的代價來估算出資料是否真實存在,相比於傳統的 List、Set、Map 等資料結構,它更高效、佔用空間更少,但是缺點是其返回的結果是概率性的,而不是確切的。

布隆過濾器的資料結構是一個大型的位陣列,而如果我們要對映一個值到布隆過濾器中,我們還需要使用多個不同的雜湊函式來生成多個雜湊值,並對每個生成的雜湊值指向的位置設定為1。查詢key是否存在時,每個雜湊函式都利用這個key計算出一個雜湊值,再根據雜湊值計算一個位置。然後對比這些雜湊函式在位陣列中對應位置的數值,如果這幾個位置中,有一個的位置值為0,則說明過濾器中不存在這個key。如果這幾個位置中,所有位置的值都是1,就說明這個布隆過濾器中,極有可能存在這個key。之所以不是百分之百確定,是因為也可能是其他的key運算導致該位置為1。

加分回答

過小的布隆過濾器bit位很快就會都被置為1,那麼查詢任何值都會返回“可能存在”,這就起不到過濾的目的了。這說明布隆過濾器的長度會直接影響誤報率,布隆過濾器越長其誤報率越小。

另外,雜湊函式的個數也需要權衡,個數越多則布隆過濾器 bit 位置位為 1 的速度越快,且布隆過濾器的效率越低。但是如果太少的話,誤報率會變高。

布隆過濾器的典型應用有:

  • 資料庫防止穿庫。 Google Bigtable,HBase 和 Cassandra 以及 Postgresql 使用BloomFilter來減少不存在的行或列的磁碟查詢。避免代價高昂的磁碟查詢會大大提高資料庫查詢操作的效能。
  • 業務場景中判斷使用者是否閱讀過某視訊或文章,比如抖音或頭條,當然會導致一定的誤判,但不會讓使用者看到重複的內容。
  • 快取宕機、快取擊穿場景,一般判斷使用者是否在快取中,如果在則直接返回結果,不在則查詢db,如果來一波冷資料,會導致快取大量擊穿,造成雪崩效應,這時候可以用布隆過濾器當快取的索引,只有在布隆過濾器中,才去查詢快取,如果沒查詢到,則穿透到db。如果不在布隆器中,則直接返回。
  • WEB攔截器,如果相同請求則攔截,防止重複被攻擊。使用者第一次請求,將請求引數放入布隆過濾器中,當第二次請求時,先判斷請求引數是否被布隆過濾器命中。可以提高快取命中率。Squid 網頁代理快取伺服器在 cache digests 中就使用了布隆過濾器。Google Chrome瀏覽器使用了布隆過濾器加速安全瀏覽服務。
  • Venti 文件儲存系統也採用布隆過濾器來檢測先前儲存的資料。
  • SPIN 模型檢測器也使用布隆過濾器在大規模驗證問題時跟蹤可達狀態空間。

Redis 因其支援 setbit 和 getbit 操作,且純記憶體效能高等特點,因此天然就可以作為布隆過濾器來使用。但是布隆過濾器的不當使用極易產生大 Value這會增加 Redis 阻塞風險,因此實際使用中建議對體積龐大的布隆過濾器進行拆分。拆分的形式方法多種多樣,但是本質是不要將 Hash(Key) 之後的請求分散在多個節點的多個小 bitmap 上,而是應該拆分成多個小 bitmap 之後,對一個 Key 的所有雜湊函式都落在這一個小 bitmap 上。

————————————————

5-有一種將字母編碼成數字的方式:'a'->1, 'b->2', ... , 'z->26'。

現在給一串數字,返回有多少種可能的譯碼結果 示例1 輸入"12" 輸出2
import java.util.*;
 
public class Solution {
    /**
     * 解碼
     * @param nums string字串 數字串
     * @return int整型
     */
    public int solve (String nums) {
        // write code here
        int len = nums.length();
        int[] possible = new int[len+1];
        possible[0] = 1;
        for (int i = 0; i < len; i++) {
            // 查表求 possible[i+1]
            // 1 個字元場景的可能
            int pos = 0;
            if (nums.charAt(i) != '0') {
                pos += possible[i];
            }
            // 2 個字元場景的可能
            if (i != 0 && nums.charAt(i-1) != '0' && Integer.parseInt(nums.substring(i-1, i+1)) <= 26) {
                pos += possible[i-1];
            }
            possible[i+1] = pos;
        }
        return possible[len];
    }
}