阿里長達57分鐘的電話一面,全程無尿點!(Java開發)

整個面試分三塊進行:

一、基礎知識,主要問了計網和數據庫以及算法

  • TCP 第三次握手和四次揮手過程

騰訊的面試官大大也問了這個問題,並且特別提醒了我要注意記住 tcp 連接和斷開時客戶端和服務器端的狀態,真是超級感謝啊,這點原來還真的一直沒有注意過。

首先我們來回顧一下 TCP 的數據傳輸單元,TCP 傳送的數據單元稱爲報文段。一個 TCP 報文段分爲 TCP 首部和 TCP 數據兩部分,整個 TCP 報文段都封裝在 IP 數據報中的數據部分,TCP 首部長度是4的整數倍,其中有固定的20個字節,剩餘的可變動的就是選項和填充「最常見的可選字段是最長報文大小,又稱爲MSS(Maximum Segment Size),每個連接方通常都在通信的第一個報文段(爲建立連接而設置SYN標誌爲1的那個段)中指明這個選項,它表示本端所能接受的最大報文段的長度。」,20個固定的字節包括了源端口號(2 字節)、目的端口(2字節)、seq序列號(4字節)、確認號ack(4字節)、以及確認位ACK 等等。

其次,我們來詳細講解一下三次握手、四次揮手的過程:

 

三次握手

 

首先,在三次握手建立連接的階段,是不會傳輸 TCP 報文段的,傳輸的是 傳輸控制塊(TCB),傳輸控制塊 TCB(Transmission Control Block)存儲了每一個連接中的一些重要信息,如:TCP 連接表,指向發送和接收緩存的指針,指向重傳隊列的指針,當前的發送和接收序號等等。

最開始的 Client 和 Server 都是處於 Closed,由於服務器端不知道要跟誰建立連接,所以其只能被動打開,然後監聽端口,此時 Server 處於 Listen 狀態;

而 Client 會主動打開,然後構建好 TCB 「SYN= 1,seq = x」,發送給服務器端,此時 Client 會將狀態置爲 SYN_SEND 「同步已發送」;

服務器端收到客戶端發來的同步請求後,會將狀態置爲 SYN_RECV「同步已接收」,同時會構建好 TCB「SYN = 1,seq = y,ACK = 1,ack = x + 1」發送給客戶端;

客戶端接收到了服務器端發來的傳輸控制塊之後,會將自己的狀態改爲 ESTABLISHED「建立連接」,然後發送確認報文(ACK= 1,seq = x + 1,ack = y + 1);

服務器端在收到了客戶端發來的報文之後,也將狀態置爲 ESTABLISHED「建立連接」,至此,三次握手結束,當然在這裏,可以帶 tcp 報文段信息過來了,因爲此時客戶端已經可以保證是可靠的傳輸了,所以在這一端可以發送報文段了。

 

幾個問題:

爲何不直接在第一次握手就帶上報文段消息,非要第三次纔可以帶?

因爲 TCP 是要保證數據的不丟失且可靠,如果在第一次就帶上報文段消息,此次建立連接很有可能就會失敗,那麼就不能保證數據的不丟失了,在不可靠的機制上進行這種操作,換來的代價太大,每次發送報文段的資源也會增大,得不償失;

而第三次握手的時候,客戶端已經知道服務器端準備好了,所以只要告訴服務器端自己準備好了就okay了,所以此時帶上報文段信息沒有任何問題。

可不可以只握手兩次?

肯定是不可以的,三次握手主要是解決這樣一個常見的問題,客戶端發送了第一個請求連接並且沒有丟失,只是因爲在網絡結點中滯留的時間太長了,由於TCP的客戶端遲遲沒有收到確認報文,因爲服務器沒有收到,此時重新向服務器發送這條報文,此後客戶端和服務器經過兩次握手完成連接,傳輸數據,然後關閉連接。此時此前滯留的那一次請求連接,網絡通暢了到達了服務器,這個報文本該是失效的,但是,兩次握手的機制將會讓客戶端和服務器再次建立連接,這將導致不必要的錯誤和資源的浪費。

如果採用的是三次握手,就算是那一次失效的報文傳送過來了,服務端接受到了那條失效報文並且回覆了確認報文,但是客戶端不會再次發出確認。由於服務器收不到確認,就知道客戶端並沒有請求連接。

(版權聲明:本文爲CSDN博主「小書go」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。原文鏈接:http://blog.csdn.net/qzcsu/article/details/72861891)

 

四次揮手

最開始客戶端和服務器端都是 ESTABLISHED 的狀態,然後客戶端會主動關閉,而服務器端則是被動關閉。

客戶端發送一個 FIN 報文段,seq = 結束的報文段序號 + 1「假設爲 u」,告訴服務器端,客戶端需要關閉了,此時將客戶端的狀態變爲 FIN-WAIT-1,等待服務器端的反饋;

服務器在接收到了客戶端發來的 FIN 包之後,會發一條 ack報文反饋給客戶端,其中報文中包括 ACK = 1,seq = v,ack = u+1,告訴客戶端收到了客戶端要關閉的消息了,同時服務器端會通知應用進程需要關閉連接了,並將自己的狀態置爲 CLOSE-WAIT;

由於服務器端可能還有一些數據沒處理完,所以需要一段時間的等待,當處理完了之後,會再發一條報文,其中 FIN = 1,ACK = 1,seq = w,ack = u+1,告知客戶端,服務器端現在可以關閉了,並將服務器端的狀態由 CLOSE-WAIT 變爲 LAST-ACK;

客戶端在收到了服務器端發來的消息之後,會發一條ack報文「ACK = 1,seq = u+1,ack = w+1」回去,告知服務器端,客戶端已經知道了你準備好關閉了,此時會將客戶端的狀態由 FIN-WAIT-2 置爲 TIME-WAIT,在兩個最長報文段傳輸時間過後,會自動將客戶端的狀態由 TIME-WAIT 置爲 CLOSED。

服務器端收到消息之後,就將狀態由 LAST-ACK 只爲了 CLOSED,自此,四次揮手全部結束。

 

一個很常見的問題,爲何不能三次揮手呢?

首先如果去掉最後一次揮手,那麼服務器端就不知道自己要關閉的報文有沒有傳輸成功,可能半路上就失敗了,但是此時客戶端不知道,導致客戶端一直在等待服務器關閉,但是此時服務器端直接就關閉了;

如果中間的兩次揮手合併,那是肯定不行的,因爲此時服務器端可能還有很多報文未處理完,此時直接關閉肯定會對傳輸有很大影響。

爲什麼客戶端在收到 服務器端發來的 FIN 包後要等 2 個最長報文段傳輸時間?

防止最後自己發去的 ack 沒傳送到服務器,如果服務器沒收到客戶端的 ack,肯定會選擇重發一次 FIN 包,那麼此時如果客戶端已經關閉了,客戶端就不能再發 ack 確認收到了,至於爲何是 2 個報文段傳輸時間,因爲剛好一去一回嘛… 2 個最長報文傳輸時間沒有 FIN 包發來,就說明服務器已經關閉了,客戶端也就可以安心關閉了。

  • http 和 tcp 的區別

tcp 是傳輸層協議,http是應用層協議,http在傳輸層就是使用的 tcp。

  • 排序算法有哪些

這個簡單。排序算法分爲比較算法和非比較算法,其中比較算法包括交換排序「冒泡和快排」、選擇排序「簡單選擇排序和堆排序」、插入排序「直接插入排序、希爾排序」、歸併排序「二路歸併和多路歸併」,非比較排序有計數排序、桶排序、基數排序。「公式:不穩定的有:快些選堆」

冒泡排序。穩定的,平均是間複雜度爲 O(n²),最好時間複雜度那肯定就是一次循環 O(n),最壞時間複雜度爲 O(n²)。空間複雜度 O(1)。

快速排序。不穩定,平均時間複雜度爲O(nlogn),最好的時間複雜度爲O(nlogn),最壞就是選定的基準值在最邊上,這樣就是O(n²),注意哦,快排的空間複雜度平均是 O(logn),最差 O(n)。

簡單選擇排序。不穩定,平均、最好、最壞時間複雜度都爲O(n²)。空間複雜度 O(1)。

堆排序。不穩定,平均、最好、最壞的時間複雜度爲O(nlogn)。空間複雜度 O(1)。

直接插入排序。穩定。最好O(n),平均、最壞時間複雜度O(n²)。空間複雜度 O(1)。

希爾排序。不穩定。最好O(n),平均O(n1.3),最壞肯定是O(n²)。空間複雜度O(1)。

歸併排序。穩定。最好、最壞、最差時間複雜度O(nlogn),空間複雜度O(n)。

計數排序。穩定,空間換時間。適合數比較集中在一起的,這樣k就少了,時間複雜度爲 O(n+k),空間複雜度也爲O(n+k)。「個人還是覺得其實空間複雜度爲O(k),因爲我可以把值放回去的時候可以放到原數組上,所以是O(k)。」

桶排序,桶越多,時間複雜度很簡單,爲O(n+k),空間複雜度最壞爲O(n+k),其中 n 是因爲桶內部所有元素的排序, k 是指桶的數量。

基數排序,時間複雜度O(n*k),k爲最大數的位數,空間複雜度爲O(n)。

  • 堆排序的穩定性,如何實現堆排序,具體細節

這個很簡單,就不詳細說了。

  • 歸併排序的穩定性,如何實現歸併排序,具體細節

簡單。

  • 說一下jdk自帶的排序用到了哪些排序算法,展開講一下

Arrays.sort() & Collections.sort()

JDK中的自帶的排序算法實現原理精彩總結

jdk層面實現的sort總共是兩類,一個是 Arrays.sort(), Collections.sort();

Arrays.sort()

如果數組內元素是基本數據類型,最主要採用的是雙軸快速排序「其實就是三路快排一模一樣的思路,只不過三路快排中間是 = pivot1,而雙軸快速排序是(pivot1,pivot2),具體戳鏈接:http://www.cnblogs.com/nullzx/p/5880191.html 。

總結就是數組長度小於47的時候是用直接插入算法,大於47並且小於286是採用雙軸快速排序,大於286如果連續性好「也就是元素大多有序,有一個flag專門用來記錄數組元素的升降次數,代表這個數組的連續性」採用的是歸併排序,否則還是依舊採用雙軸快速排序。

如果數組內元素是對象,採用的是TimSort.sort(),跟 Collections.sort()一樣,都是採用的這個函數,這是歸併排序算法和插入排序的結合。

Collections.sort(),採用 TimSort.sort()。

TimSort.sort() 大概原理:

當待排序元素小於32個時,採用二分插入排序,是插入排序的一種改進,可以減少插入排序比較的次數。當找到插入位置時,直接利用System.copy()函數即可。

當待排序元素大於等於32個時,進行歸併排序(和傳統的歸併不一樣),首先將排序元素分區,每個分區的大小區間爲[16,32),然後依次對每個分區進行排序(具體實現依然是二分插入排序),排完序的分區壓入棧(準確的說不是棧,而是一個數組,用來記錄排序好的分區),當棧內的分區數滿足條件時,進行分區合併,合併爲一個更大的分區,當棧中只剩一個分區時,排序完成。

  • mysql如何優化

建索引

  • 索引的建立的原則有哪些

除了運用最左前綴、索引下推、考慮索引長度,還有哪些是建立索引需要考慮的 「這個實在想不到了」

  • 紅黑樹和平衡二叉樹的區別,各自的優勢特點,以及紅黑樹如何進行添加數據「具體說一下旋轉過程,我只說了我博客上寫了,具體的給忘了…」

這個二面真得複習複習。

二、java方面

  • 講一下雙親委派模型,爲什麼要設計雙親委派模型

雙親委派模型:一個類加載器在加載類時,先把這個請求委託給自己的父類加載器去執行,如果父類加載器還存在父類加載器,就繼續向上委託,直到頂層的啓動類加載器。如果父類加載器能夠完成類加載,就成功返回,如果父類加載器無法完成加載,那麼子加載器纔會嘗試自己去加載。

好處:java類隨着它的類加載器一起具備了一種帶有優先級的層次關係

這種雙親委派模式的設計原因:可以避免類的重複加載,另外也避免了java的核心API被篡改。

  • 違反雙親委派模型,我不小心說了jdbc,然後問我jdbc是如何連接到數據庫的,具體流程是什麼,我就說了個反射…沒複習到位

《從雙親委派模型到jdbc》http://jeromememory.github.io/2020/03/19/%E4%BB%8E%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%A8%A1%E5%9E%8B%E5%88%B0%20jdbc.html

  • 講一下jmm,爲何這樣設計

java memeory model ,java 內存模型,設計的目的是屏蔽掉各種硬件和操作系統之間的差異性,實現讓 Java 在各種平臺下都能能到一致的併發效果。jmm 中,分爲主內存和工作內存,其中每個線程擁有自己的工作內存,而主內存是所有線程共享的。這裏我遇到疑問了,在周志華老師那本書中,先是講主內存中存儲了所有的變量,那必然就包括了線程的局部變量,那難道我線程使用自己的局部變量,也要從主內存中拷貝一份副本到工作內存中呢?這是不是和說主內存是共享區域產生了矛盾呢?書中還說可以類比,主內存就是跟堆差不多,而工作內存類似於棧,那之前說的主內存存儲了所有的變量,這句話是不是有問題呢?個人覺得主內存不可能存儲所有的變量…應該就是類似於堆存儲共享變量…

  • 爲何要有工作內存,有了主內存和工作內存不是更麻煩啊,要不斷的複製移動數據,爲何不能直接對主內存操作「這個也沒答上來」

這就跟爲何要提出寄存器和緩存一樣的道理,如果所有的操作都在內存中完成,那速度實在是太慢了,只有工作在寄存器和緩存中,速度才能讓人滿意,而這裏的主內存就類比爲內存,工作內存就類比爲寄存器和緩存。

  • 什麼是線程安全 「多線程方面一個問題都沒問…血虧」

多個線程訪問一個對象,無需考慮環境和額外同步,調用這個對象的行爲就能得到正確的答案,就說明這個對象是線程安全的。

  • 舉三個例子分別描述 jmm的三個特性「原子性、有序性、可見性」導致的線程安全問題

不遵循原子性:volatile 變量的自加,複合操作,導致線程不安全;

不遵循有序性:比如共享變量,多個線程同時訪問,不按序,每個都拷貝一份到自己的工作內存,必然會導致線程不安全;

不遵循可見性:普通變量,跟有序性一樣的例子,每個都從主內存拷貝一份變量的副本到工作內存,必然會導致線程不安全。

  • 講一下 RunTimeException 的造成的原因「非檢查型異常」,並說一下爲什麼不處理 RunTimeException?

RuntimeException是Exception子類。而Exception還有其它類型的異常,我們統一稱爲非Runtime異常。RuntimeException的特點是非檢查型異常,也就是Java系統中允許可以不被catch,在運行時拋出。而其它定非運行時異常如果拋出的話必須顯示的catch,否則編譯不過。

RuntimeException常見異常:

1 NullPointerException,空指針異常。

2 NumberFormatException,字符串轉化成數字時。

3 ArrayIndexOutOfBoundsException, 數組越界時。

4 StringIndexOutOfBoundsException, 字符串越界時。

5 ClassCastException,類型轉換時。

6 UnsupportedOperationException,該操作不支持,一般子類不實現父類的某些方法時。

7 ArithmeticException,零作爲除數等。

8 IllegalArgumentException,表明傳遞了一個不合法或不正確的參數

運行時出現錯誤,說明你的代碼有問題,程序已經無法繼續運行,所以對RuntimeException的處理時不必要的。之所以不處理,目的就是要是程序停止,修正代碼。

三、項目

  • 主要就問了下我最近在做什麼項目,到什麼階段了,有多少人用;
  • 問我爬蟲的實現「面試官貌似沒有做過這方面的東西」
  • 我提了一嘴 xxl-job,面試官應該也沒用過,就沒有深究,本來還想講講kafka的,結果直接沒問…白準備了

最後就問了一個很常見的算法問題:

256M 的內存如何對 16g的數組進行排序

多路歸併,因爲沒要求存儲,只要求了內存,可以多路歸併,加入每個元素都是 1M,則可以最多分成 256 組,然後進行歸併。

具體描述:採用外部排序,先將16 g數組分成 256 M 一組,然後分別讀入內存進行內部排序「比如說可以使用快排」,將這些組內元素全部排好序之後,然後運用敗者樹和置換-選擇排序,進行多路歸併,即可。

這裏其實可以引申出好多問題:

  1. 海量數據 求最大的 K個數問題,如何解決?

按位劃分區域,可以儘快的縮小範圍,比如最高位 0 分一堆,1 分成一堆而且不用排序,這是第一選擇。

最經典的方法當然是 堆 了,比如要求前1000個最大的數,那就直接建一個 1000 大小的小根堆,然後遍歷,只要發現後面的數比小根堆的根節點大,就把根節點和該數交換,重新調整堆,遍歷完之後,堆中的數自然就是最大的 1000 個數了;

當然能使用堆排序的前提是內存中要能夠放得下這個 K,如果放不下呢?那就只能外部排序了,排序完之後拿到第 K 大的數即可,當然排序前可以和方法一搭配一下。

  1. 海量數據求中位數,如何解決?

可以按照位來分組,比如說最高位是0的一組,是 1 的一組,這樣可以統計出那一組更少,這樣就排除了一大半,然後繼續這樣排查,最終縮小範圍後直接內部排序。

直接外部排序,然後取中間值,最笨的方法。

  1. 在海量數據中找出出現頻率最高的前k個數,例如,在搜索引擎中,統計搜索最熱門的10個查詢詞;在歌曲庫中統計下載最高的前10首歌曲。

如果重複率很高,可以採用前綴樹,因爲 trie 樹適用於數據量大,重複多,但是數據種類小必須得可以放入內存;

按照 hash 進行分組,這樣就能避免相同的數分到不同區域去了,導致不好統計。hash 分組完畢後,然後用前綴樹 或者 hashmap 來計算每個組的前 k 個頻率最高的數,最後對各個組的前 k 個數進行統計即可。

  1. 給40億個不重複的unsigned int的整數,沒排過序的,然後再給一個數,如何快速判斷這個數是否在那40億個數當中?

這裏我們把40億個數中的每一個用32位的二進制來表示 假設這40億個數開始放在一個文件中。

然後將這40億個數分成兩類: 1.最高位爲0 2.最高位爲1 並將這兩類分別寫入到兩個文件中,其中一個文件中數的個數<=20億,而另一個>=20億(這相當於折半了);與要查找的數的最高位比較並接着進入相應的文件再查找

再然後把這個文件爲又分成兩類: 1.次最高位爲0 2.次最高位爲1

並將這兩類分別寫入到兩個文件中,其中一個文件中數的個數<=10億,而另一個>=10億(這相當於折半了);與要查找的數的次最高位比較並接着進入相應的文件再查找。……. 以此類推,就可以找到了,而且時間複雜度爲O(logn)。

大概統計一下,海量數據求 TopK 的普遍方法:

  • 最快的不需要排序就能排除一大堆的數據的方法就是看 “位”,比如最高位爲 0 的分一塊,爲 1 的分一塊,這樣迅速就分出一大塊不需要的了,尤其適合找中位數,等分的差不多了就可以進行內部排序了。
  • 堆排序,適用於求海量數據最大 K or 最小的 K 個數;
  • 分治hash,適用於那些內存很小,數據很大,但是又想求最大的 K 個衆數的問題,可以先 hash 到很多個組,然後在組內部使用 hashmap 或者 前綴樹 「google等字符」,取到組內前 K 個衆數,最後進行組間比較久okay了;
  • 當然不能忘了萬能法,那就是外部排序,然後再進行相應的處理。

最後的最後,讓我們再來做個附加題:

先來看幾個比較常見的例子

  • 字處理軟件中,需要檢查一個英語單詞是否拼寫正確
  • 在 FBI,一個嫌疑人的名字是否已經在嫌疑名單上
  • 在網絡爬蟲裏,一個網址是否被訪問過
  • yahoo, gmail等郵箱垃圾郵件過濾功能

這幾個例子有一個共同的特點: 如何判斷一個元素是否存在一個集合中?

這裏必須介紹一下 bitmap 這個方法了,例如我要從海量數據中找一個數是否出現過,就可以用位圖的思路去做,如果數字是 7 ,那就在第 7 位 置 1,如果該位置已經是 1 了,那就代表出現過,不用更改。

如果問題變爲從海量數據中找一個數是否出現過一次,那這個時候就得用 2 bitmap 來表示了,也就是一個數如果出現一次,置爲 01 ,出現過兩次,置爲 10,然後再出現,都是10,這個時候如果我們只用一位,是不能表示出出現的次數的。

至於我們常說的布隆過濾器,其實也就是在bitmap之前進行一個hash,例如將字符串進行hash成數組,然後使用位圖,解決這類問題。

整理了一套 5000 頁的 Java 學習手冊,,新鮮出爐,分享給大家!此手冊內容專注 Java技術,包括 JavaWeb,SSM,Linux,Spring Boot,MyBatis,MySQL,Nginx,Git,GitHub,Servlet,IDEA,多線程,集合,JVM,DeBug, Dubbo,Redis,算法,面試題等相關內容。

 

 

領取方式

1.首先轉發文章+關注樓主

2.然後私信回覆關鍵字【手冊】即可獲得【5000頁的Java學習手冊】的免費領取方式

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