騰訊面試體驗(yàn)倍兒好,我們一起感受一下吧!
大家好,我是小林。
今天分享一位讀者的騰訊春招實(shí)習(xí)面經(jīng),崗位Java后端,主要問了MySQL、Java、網(wǎng)絡(luò)這三大塊。
他覺得這場面試非常有收獲,雖然都是基礎(chǔ)問題,但是往下深挖根莖葉脈全部相連。反問環(huán)節(jié)面試官給了我很多建議,包括面試、策略、基礎(chǔ)、算法等等,是一次寶貴的學(xué)習(xí)經(jīng)歷。
MySQL
介紹一下MySQL的索引機(jī)制
索引可以幫助我們快速搜索數(shù)據(jù),innodb 存儲(chǔ)引擎用的是 b+樹索引,葉子節(jié)點(diǎn)存放的是索引+數(shù)據(jù),非葉子節(jié)點(diǎn)只存放索引。
可以按照四個(gè)角度來分類索引。
- 按「數(shù)據(jù)結(jié)構(gòu)」分類:B+tree索引、Hash索引、Full-text索引。
- 按「物理存儲(chǔ)」分類:聚簇索引(主鍵索引)、二級索引(輔助索引)。
- 按「字段特性」分類:主鍵索引、唯一索引、普通索引、前綴索引。
- 按「字段個(gè)數(shù)」分類:單列索引、聯(lián)合索引。
聯(lián)合索引是什么?
通過將多個(gè)字段組合成一個(gè)索引,該索引就被稱為聯(lián)合索引。
比如,將商品表中的 product_no 和 name 字段組合成聯(lián)合索引(product_no, name),創(chuàng)建聯(lián)合索引的方式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
聯(lián)合索引(product_no, name) 的 B+Tree 示意圖如下(圖中葉子節(jié)點(diǎn)之間我畫了單向鏈表,但是實(shí)際上是雙向鏈表,原圖我找不到了,修改不了,偷個(gè)懶我不重畫了,大家腦補(bǔ)成雙向鏈表就行)。
聯(lián)合索引
可以看到,聯(lián)合索引的非葉子節(jié)點(diǎn)用兩個(gè)字段的值作為 B+Tree 的 key 值。當(dāng)在聯(lián)合索引查詢數(shù)據(jù)時(shí),先按 product_no 字段比較,在 product_no 相同的情況下再按 name 字段比較。
也就是說,聯(lián)合索引查詢的 B+Tree 是先按 product_no 進(jìn)行排序,然后再 product_no 相同的情況再按 name 字段排序。
因此,使用聯(lián)合索引時(shí),存在最左匹配原則,也就是按照最左優(yōu)先的方式進(jìn)行索引的匹配。在使用聯(lián)合索引進(jìn)行查詢的時(shí)候,如果不遵循「最左匹配原則」,聯(lián)合索引會(huì)失效,這樣就無法利用到索引快速查詢的特性了。
什么是聚簇索引?
聚簇索引的 B+Tree 的葉子節(jié)點(diǎn)存放的是實(shí)際數(shù)據(jù),所有完整的用戶記錄都存放在主鍵索引的 B+Tree 的葉子節(jié)點(diǎn)里。
什么是覆蓋索引?
在查詢時(shí)使用了二級索引,如果查詢的數(shù)據(jù)能在二級索引里查詢的到,那么就不需要回表,這個(gè)過程就是覆蓋索引。如果查詢的數(shù)據(jù)不在二級索引里,就會(huì)先檢索二級索引,找到對應(yīng)的葉子節(jié)點(diǎn),獲取到主鍵值后,然后再檢索主鍵索引,就能查詢到數(shù)據(jù)了,這個(gè)過程就是回表。
整個(gè)索引查詢的過程是怎樣的?
InnoDB 里的 B+ 樹中的每個(gè)節(jié)點(diǎn)都是一個(gè)數(shù)據(jù)頁,結(jié)構(gòu)示意圖如下:
B+ 樹如何實(shí)現(xiàn)快速查找主鍵為 6 的記錄,以上圖為例子:
- 從根節(jié)點(diǎn)開始,通過二分法快速定位到符合頁內(nèi)范圍包含查詢值的頁,因?yàn)椴樵兊闹麈I值為 6,在[1, 7)范圍之間,所以到頁 30 中查找更詳細(xì)的目錄項(xiàng);
- 在非葉子節(jié)點(diǎn)(頁30)中,繼續(xù)通過二分法快速定位到符合頁內(nèi)范圍包含查詢值的頁,主鍵值大于 5,所以就到葉子節(jié)點(diǎn)(頁16)查找記錄;
- 接著,在葉子節(jié)點(diǎn)(頁16)中,通過槽查找記錄時(shí),使用二分法快速定位要查詢的記錄在哪個(gè)槽(哪個(gè)記錄分組),定位到槽后,再遍歷槽內(nèi)的所有記錄,找到主鍵為 6 的記錄。
可以看到,在定位記錄所在哪一個(gè)頁時(shí),也是通過二分法快速定位到包含該記錄的頁。定位到該頁后,又會(huì)在該頁內(nèi)進(jìn)行二分法快速定位記錄所在的分組(槽號),最后在分組內(nèi)進(jìn)行遍歷查找。
事務(wù)的隔離級別有哪些?
- 讀未提交,指一個(gè)事務(wù)還沒提交時(shí),它做的變更就能被其他事務(wù)看到;
- 讀提交,指一個(gè)事務(wù)提交之后,它做的變更才能被其他事務(wù)看到;
- 可重復(fù)讀,指一個(gè)事務(wù)執(zhí)行過程中看到的數(shù)據(jù),一直跟這個(gè)事務(wù)啟動(dòng)時(shí)看到的數(shù)據(jù)是一致的,MySQL InnoDB 引擎的默認(rèn)隔離級別;
- 串行化;會(huì)對記錄加上讀寫鎖,在多個(gè)事務(wù)對這條記錄進(jìn)行讀寫操作時(shí),如果發(fā)生了讀寫沖突的時(shí)候,后訪問的事務(wù)必須等前一個(gè)事務(wù)執(zhí)行完成,才能繼續(xù)執(zhí)行;
臟讀、幻讀、不可重讀分別是什么意思?
- 臟讀:如果一個(gè)事務(wù)「讀到」了另一個(gè)「未提交事務(wù)修改過的數(shù)據(jù)」,就意味著發(fā)生了「臟讀」現(xiàn)象。
- 幻讀:在一個(gè)事務(wù)內(nèi)多次查詢某個(gè)符合查詢條件的「記錄數(shù)量」,如果出現(xiàn)前后兩次查詢到的記錄數(shù)量不一樣的情況,就意味著發(fā)生了「幻讀」現(xiàn)象。
- 不可重復(fù)讀:在一個(gè)事務(wù)內(nèi)多次讀取同一個(gè)數(shù)據(jù),如果出現(xiàn)前后兩次讀到的數(shù)據(jù)不一樣的情況,就意味著發(fā)生了「不可重復(fù)讀」現(xiàn)象。
InnoDB 多版本并發(fā)控制的具體原理,底層細(xì)節(jié)?
對于「讀提交」和「可重復(fù)讀」隔離級別的事務(wù)來說,它們是通過 Read View 來實(shí)現(xiàn)的,它們的區(qū)別在于創(chuàng)建 Read View 的時(shí)機(jī)不同,大家可以把 Read View 理解成一個(gè)數(shù)據(jù)快照,就像相機(jī)拍照那樣,定格某一時(shí)刻的風(fēng)景。
- 「讀提交」隔離級別是在「每個(gè)select語句執(zhí)行前」都會(huì)重新生成一個(gè) Read View;
- 「可重復(fù)讀」隔離級別是執(zhí)行第一條select時(shí),生成一個(gè) Read View,然后整個(gè)事務(wù)期間都在用這個(gè) Read View。
Read View 有四個(gè)重要的字段:
- m_ids :指的是在創(chuàng)建 Read View 時(shí),當(dāng)前數(shù)據(jù)庫中「活躍事務(wù)」的事務(wù) id 列表,注意是一個(gè)列表,“活躍事務(wù)”指的就是,啟動(dòng)了但還沒提交的事務(wù)。
- min_trx_id :指的是在創(chuàng)建 Read View 時(shí),當(dāng)前數(shù)據(jù)庫中「活躍事務(wù)」中事務(wù) id 最小的事務(wù),也就是 m_ids 的最小值。
- max_trx_id :這個(gè)并不是 m_ids 的最大值,而是創(chuàng)建 Read View 時(shí)當(dāng)前數(shù)據(jù)庫中應(yīng)該給下一個(gè)事務(wù)的 id 值,也就是全局事務(wù)中最大的事務(wù) id 值 + 1;
- creator_trx_id :指的是創(chuàng)建該 Read View 的事務(wù)的事務(wù) id。
對于使用 InnoDB 存儲(chǔ)引擎的數(shù)據(jù)庫表,它的聚簇索引記錄中都包含下面兩個(gè)隱藏列:
- trx_id,當(dāng)一個(gè)事務(wù)對某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),就會(huì)把該事務(wù)的事務(wù) id 記錄在 trx_id 隱藏列里;
- roll_pointer,每次對某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把舊版本的記錄寫入到 undo 日志中,然后這個(gè)隱藏列是個(gè)指針,指向每一個(gè)舊版本記錄,于是就可以通過它找到修改前的記錄。
在創(chuàng)建 Read View 后,我們可以將記錄中的 trx_id 劃分這三種情況:
一個(gè)事務(wù)去訪問記錄的時(shí)候,除了自己的更新記錄總是可見之外,還有這幾種情況:
- 如果記錄的 trx_id 值小于 Read View 中的 min_trx_id 值,表示這個(gè)版本的記錄是在創(chuàng)建 Read View 前已經(jīng)提交的事務(wù)生成的,所以該版本的記錄對當(dāng)前事務(wù)可見。
- 如果記錄的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示這個(gè)版本的記錄是在創(chuàng)建 Read View 后才啟動(dòng)的事務(wù)生成的,所以該版本的記錄對當(dāng)前事務(wù)不可見。
- 如果記錄的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之間,需要判斷 trx_id 是否在 m_ids 列表中:
- 如果記錄的 trx_id 在 m_ids 列表中,表示生成該版本記錄的活躍事務(wù)依然活躍著(還沒提交事務(wù)),所以該版本的記錄對當(dāng)前事務(wù)不可見。
- 如果記錄的 trx_id 不在 m_ids列表中,表示生成該版本記錄的活躍事務(wù)已經(jīng)被提交,所以該版本的記錄對當(dāng)前事務(wù)可見。
這種通過「版本鏈」來控制并發(fā)事務(wù)訪問同一個(gè)記錄時(shí)的行為就叫 MVCC(多版本并發(fā)控制)。
next Key是什么,怎么實(shí)現(xiàn)?
Next-Key Lock 稱為臨鍵鎖,是 Record Lock + Gap Lock 的組合,鎖定一個(gè)范圍,并且鎖定記錄本身。
假設(shè),表中有一個(gè)范圍 id 為(3,5] 的 next-key lock,那么其他事務(wù)即不能插入 id = 4 記錄,也不能修改 id = 5 這條記錄。
所以,next-key lock 即能保護(hù)該記錄,又能阻止其他事務(wù)將新紀(jì)錄插入到被保護(hù)記錄前面的間隙中。
索引失效的場景有哪些,你知道什么改進(jìn)方法嗎?
- 當(dāng)我們使用左或者左右模糊匹配的時(shí)候,也就是 like %xx 或者 like %xx%這兩種方式都會(huì)造成索引失效;
- 當(dāng)我們在查詢條件中對索引列使用函數(shù),就會(huì)導(dǎo)致索引失效。
- 當(dāng)我們在查詢條件中對索引列進(jìn)行表達(dá)式計(jì)算,也是無法走索引的。
- MySQL 在遇到字符串和數(shù)字比較的時(shí)候,會(huì)自動(dòng)把字符串轉(zhuǎn)為數(shù)字,然后再進(jìn)行比較。如果字符串是索引列,而條件語句中的輸入?yún)?shù)是數(shù)字的話,那么索引列會(huì)發(fā)生隱式類型轉(zhuǎn)換,由于隱式類型轉(zhuǎn)換是通過 CAST 函數(shù)實(shí)現(xiàn)的,等同于對索引列使用了函數(shù),所以就會(huì)導(dǎo)致索引失效。
- 聯(lián)合索引要能正確使用需要遵循最左匹配原則,也就是按照最左優(yōu)先的方式進(jìn)行索引的匹配,否則就會(huì)導(dǎo)致索引失效。
- 在 WHERE 子句中,如果在 OR 前的條件列是索引列,而在 OR 后的條件列不是索引列,那么索引會(huì)失效。
提交事務(wù)的一整個(gè)過程,每個(gè)日志都是怎么工作的?
具體更新一條記錄 UPDATE t_user SET name = 'xiaolin' WHERE id = 1; 的流程如下:
- 執(zhí)行器負(fù)責(zé)具體執(zhí)行,會(huì)調(diào)用存儲(chǔ)引擎的接口,通過主鍵索引樹搜索獲取 id = 1 這一行記錄:
如果 id=1 這一行所在的數(shù)據(jù)頁本來就在 buffer pool 中,就直接返回給執(zhí)行器更新;
如果記錄不在 buffer pool,將數(shù)據(jù)頁從磁盤讀入到 buffer pool,返回記錄給執(zhí)行器。
- 執(zhí)行器得到聚簇索引記錄后,會(huì)看一下更新前的記錄和更新后的記錄是否一樣:
如果一樣的話就不進(jìn)行后續(xù)更新流程;
如果不一樣的話就把更新前的記錄和更新后的記錄都當(dāng)作參數(shù)傳給 InnoDB 層,讓 InnoDB 真正的執(zhí)行更新記錄的操作;
- 開啟事務(wù), InnoDB 層更新記錄前,首先要記錄相應(yīng)的 undo log,因?yàn)檫@是更新操作,需要把被更新的列的舊值記下來,也就是要生成一條 undo log,undo log 會(huì)寫入 Buffer Pool 中的 Undo 頁面,不過在內(nèi)存修改該 Undo 頁面后,需要記錄對應(yīng)的 redo log。
- InnoDB 層開始更新記錄,會(huì)先更新內(nèi)存(同時(shí)標(biāo)記為臟頁),然后將記錄寫到 redo log 里面,這個(gè)時(shí)候更新就算完成了。為了減少磁盤I/O,不會(huì)立即將臟頁寫入磁盤,后續(xù)由后臺(tái)線程選擇一個(gè)合適的時(shí)機(jī)將臟頁寫入到磁盤。這就是 WAL 技術(shù),MySQL 的寫操作并不是立刻寫到磁盤上,而是先寫 redo 日志,然后在合適的時(shí)間再將修改的行數(shù)據(jù)寫到磁盤上。
- 至此,一條記錄更新完了。
- 在一條更新語句執(zhí)行完成后,然后開始記錄該語句對應(yīng)的 binlog,此時(shí)記錄的 binlog 會(huì)被保存到 binlog cache,并沒有刷新到硬盤上的 binlog 文件,在事務(wù)提交時(shí)才會(huì)統(tǒng)一將該事務(wù)運(yùn)行過程中的所有 binlog 刷新到硬盤。
- 事務(wù)提交(為了方便說明,這里不說組提交的過程,只說兩階段提交):
prepare 階段:將 redo log 對應(yīng)的事務(wù)狀態(tài)設(shè)置為 prepare,然后將 redo log 刷新到硬盤;
commit 階段:將 binlog 刷新到磁盤,接著調(diào)用引擎的提交事務(wù)接口,將 redo log 狀態(tài)設(shè)置為 commit(將事務(wù)設(shè)置為 commit 狀態(tài)后,刷入到磁盤 redo log 文件);
- 至此,一條更新語句執(zhí)行完成。
Java
JVM內(nèi)存區(qū)域 每一個(gè)區(qū)域的內(nèi)容?
JVM的內(nèi)存結(jié)構(gòu)主要分為以下幾個(gè)部分:
- 程序計(jì)數(shù)器(Program Counter Register):每個(gè)線程都有一個(gè)程序計(jì)數(shù)器。當(dāng)線程執(zhí)行 Java 方法時(shí),程序計(jì)數(shù)器保存當(dāng)前執(zhí)行指令的地址,以便在 JVM 調(diào)用其他方法或恢復(fù)線程執(zhí)行時(shí)重新回到正確的位置。
- Java 虛擬機(jī)棧(Java Virtual Machine Stacks):每個(gè)線程都有一個(gè)虛擬機(jī)棧。虛擬機(jī)棧保存著方法執(zhí)行期間的局部變量、操作數(shù)棧、方法出口等信息。線程每調(diào)用一個(gè) Java 方法時(shí),會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame),棧幀包含著該方法的局部變量、操作數(shù)棧、方法返回地址等信息。棧幀在方法執(zhí)行結(jié)束后會(huì)被彈出。
- 本地方法棧(Native Method Stack):與 Java 虛擬機(jī)棧類似,但是為本地方法服務(wù)。
- Java 堆(Java Heap):Java 堆是 Java 虛擬機(jī)中最大的一塊內(nèi)存區(qū)域,用于存儲(chǔ)各種類型的對象實(shí)例,也是垃圾收集器的主要工作區(qū)域。Java 堆是所有線程共享的部分。
- 方法區(qū)(Method Area):方法區(qū)也是所有線程共享的部分,它用于存儲(chǔ)類的加載信息、靜態(tài)變量、常量池、方法字節(jié)碼等數(shù)據(jù)。在 Java 8 及以前的版本中,方法區(qū)被實(shí)現(xiàn)為永久代(Permanent Generation),在 Java 8 中被改為元空間(Metaspace)。
JVM異常問題?
- StackOverflowError(線程請求棧深度超過虛擬機(jī)所允許的最大深度)
- OutOfMemoryError(堆內(nèi)存不夠用)
- PermGen space(方法區(qū)內(nèi)存不夠用)。
String保存在哪里呢
String 保存在字符串常量池中,不同于其他對象,它的值是不可變的,且可以被多個(gè)引用共享。
java版本改動(dòng)問題 1.7 1.8有哪些主要區(qū)別?
- Java 7 新特性:鉆石操作符、try-with-resource語句、支持動(dòng)態(tài)類型語言、Fork/Join框架等。
- Java 8 新特性:Lambda 表達(dá)式、Stream API、新的 Date/Time API、Nashorn JavaScript 引擎等。
怎么找到需要回收的垃圾?
Java中的垃圾回收機(jī)制通過判斷對象是否可達(dá)來確定哪些對象可以被回收。當(dāng)一個(gè)對象沒有任何引用指向它時(shí),它就可以被回收,這個(gè)過程由JVM的垃圾回收器自動(dòng)完成。
JVM使用可達(dá)性分析算法來判斷對象是否可達(dá)。從GC Roots對象開始,通過一系列的引用鏈來遍歷所有的對象,如果一個(gè)對象不可達(dá),則說明它已經(jīng)死亡,可以被回收了。
在Java中,有4種引用類型:強(qiáng)引用、軟引用、弱引用和虛引用。其中,強(qiáng)引用是最常見的引用類型,只要強(qiáng)引用存在,垃圾回收器就不會(huì)回收該對象。軟引用、弱引用和虛引用則分別表示對對象的軟引用、弱引用和虛引用,當(dāng)垃圾回收器進(jìn)行垃圾回收時(shí),會(huì)根據(jù)不同的引用類型來決定是否回收對象。
有哪些常見的垃圾回收器,舉幾個(gè)說說?
- Serial收集器:單線程的垃圾回收器,使用標(biāo)記-復(fù)制算法,適合小型應(yīng)用程序或客戶端應(yīng)用程序。
- Parallel收集器:多線程的垃圾回收器,使用標(biāo)記-復(fù)制算法,適合在后臺(tái)運(yùn)行的中型應(yīng)用程序。
- CMS收集器:并發(fā)垃圾回收器,使用標(biāo)記-清除算法,適合對響應(yīng)時(shí)間有要求的中型應(yīng)用程序。
- G1收集器:并發(fā)垃圾回收器,使用標(biāo)記-整理算法,適合對響應(yīng)時(shí)間有要求且堆內(nèi)存較大的應(yīng)用程序。
其中,Serial收集器和Parallel收集器是新生代收集器,CMS和G1是老年代收集器。
HashMap底層怎么實(shí)現(xiàn)的 ?線程安全嗎?
HashMap底層是基于數(shù)組和鏈表實(shí)現(xiàn)的。簡單來說,HashMap將key通過hash算法映射到數(shù)組中,然后在對應(yīng)的鏈表中查找value。當(dāng)多個(gè)key的hash值相同時(shí),會(huì)在同一個(gè)數(shù)組位置上使用鏈表來存儲(chǔ)這些key-value。但是,當(dāng)鏈表長度太長時(shí),會(huì)影響HashMap的性能,因此在JDK1.8中,當(dāng)鏈表長度超過閾值時(shí),會(huì)將鏈表轉(zhuǎn)換為紅黑樹,以提高查找效率。
HashMap不是線程安全的,因?yàn)槎鄠€(gè)線程同時(shí)訪問HashMap時(shí)可能會(huì)導(dǎo)致數(shù)據(jù)不一致的問題??梢允褂肅oncurrentHashMap來實(shí)現(xiàn)線程安全的Map。
紅黑樹是什么?
紅黑樹是一種自平衡的二叉查找樹,可以保證在最壞情況下基本動(dòng)態(tài)操作的時(shí)間復(fù)雜度為O(log n)。紅黑樹中的每個(gè)節(jié)點(diǎn)都有一個(gè)顏色屬性,可以是紅色或黑色。紅黑樹滿足以下5個(gè)性質(zhì):
- 每個(gè)節(jié)點(diǎn)要么是紅色,要么是黑色。
- 根節(jié)點(diǎn)是黑色的。
- 每個(gè)葉子節(jié)點(diǎn)(NIL節(jié)點(diǎn),空節(jié)點(diǎn))是黑色的。
- 如果一個(gè)節(jié)點(diǎn)是紅色的,則它的兩個(gè)子節(jié)點(diǎn)都是黑色的。
- 對于每個(gè)節(jié)點(diǎn),從該節(jié)點(diǎn)到其所有后代葉子節(jié)點(diǎn)的簡單路徑上,均包含相同數(shù)目的黑色節(jié)點(diǎn)。
通過這些性質(zhì),紅黑樹可以保證在插入和刪除節(jié)點(diǎn)時(shí),自動(dòng)調(diào)整樹的結(jié)構(gòu),以保持樹的平衡和性質(zhì)的滿足。相比于普通的二叉查找樹,紅黑樹的平衡性更好,查找、插入和刪除都具有更穩(wěn)定的時(shí)間復(fù)雜度,因此在很多場景下被廣泛應(yīng)用。
什么是公平鎖和非公平鎖?
公平鎖和非公平鎖是針對鎖的獲取方式而言的。
公平鎖是指多個(gè)線程按照申請鎖的順序來獲取鎖,即先到先得的原則。當(dāng)線程A釋放鎖后,線程B、C、D依次獲取鎖,如果此時(shí)線程E申請鎖,則它需要等待B、C、D依次獲取到鎖并釋放鎖后才能獲取鎖。
非公平鎖是指多個(gè)線程獲取鎖的順序是隨機(jī)的,不保證公平性。當(dāng)線程A釋放鎖后,線程B、C、D等線程都可以通過競爭獲取到鎖,而此時(shí)線程E也可以通過競爭獲取到鎖。
在實(shí)際應(yīng)用中,公平鎖可以避免饑餓現(xiàn)象,但是由于需要維護(hù)線程隊(duì)列,因此效率相對較低。而非公平鎖由于不需要維護(hù)線程隊(duì)列,因此效率相對較高,但是可能會(huì)導(dǎo)致某些線程長時(shí)間無法獲取鎖。
ThreadLocal 是什么?
ThreadLocal是Java中的一個(gè)線程封閉技術(shù),它可以讓每個(gè)線程都擁有自己單獨(dú)的變量副本,從而保證線程安全。ThreadLocal提供了一種線程本地存儲(chǔ)的機(jī)制,為每個(gè)線程提供一個(gè)獨(dú)立的變量副本,使得每個(gè)線程中的變量互不干擾。
計(jì)算機(jī)網(wǎng)絡(luò)
TCP 三次握手、四次揮手的過程?
三次握手的過程:
TCP 三次握手
- 一開始,客戶端和服務(wù)端都處于 CLOSE 狀態(tài)。先是服務(wù)端主動(dòng)監(jiān)聽某個(gè)端口,處于 LISTEN 狀態(tài)
- 客戶端會(huì)隨機(jī)初始化序號(client_isn),將此序號置于 TCP 首部的「序號」字段中,同時(shí)把 SYN 標(biāo)志位置為 1,表示 SYN 報(bào)文。接著把第一個(gè) SYN 報(bào)文發(fā)送給服務(wù)端,表示向服務(wù)端發(fā)起連接,該報(bào)文不包含應(yīng)用層數(shù)據(jù),之后客戶端處于 SYN-SENT 狀態(tài)。
- 服務(wù)端收到客戶端的 SYN 報(bào)文后,首先服務(wù)端也隨機(jī)初始化自己的序號(server_isn),將此序號填入 TCP 首部的「序號」字段中,其次把 TCP 首部的「確認(rèn)應(yīng)答號」字段填入 client_isn + 1, 接著把 SYN 和 ACK 標(biāo)志位置為 1。最后把該報(bào)文發(fā)給客戶端,該報(bào)文也不包含應(yīng)用層數(shù)據(jù),之后服務(wù)端處于 SYN-RCVD 狀態(tài)。
- 客戶端收到服務(wù)端報(bào)文后,還要向服務(wù)端回應(yīng)最后一個(gè)應(yīng)答報(bào)文,首先該應(yīng)答報(bào)文 TCP 首部 ACK 標(biāo)志位置為 1 ,其次「確認(rèn)應(yīng)答號」字段填入 server_isn + 1 ,最后把報(bào)文發(fā)送給服務(wù)端,這次報(bào)文可以攜帶客戶到服務(wù)端的數(shù)據(jù),之后客戶端處于 ESTABLISHED 狀態(tài)。
- 服務(wù)端收到客戶端的應(yīng)答報(bào)文后,也進(jìn)入 ESTABLISHED 狀態(tài)。
四次揮手的過程:
- 客戶端打算關(guān)閉連接,此時(shí)會(huì)發(fā)送一個(gè) TCP 首部 FIN 標(biāo)志位被置為 1 的報(bào)文,也即 FIN 報(bào)文,之后客戶端進(jìn)入 FIN_WAIT_1 狀態(tài)。
- 服務(wù)端收到該報(bào)文后,就向客戶端發(fā)送 ACK 應(yīng)答報(bào)文,接著服務(wù)端進(jìn)入 CLOSE_WAIT 狀態(tài)。
- 客戶端收到服務(wù)端的 ACK 應(yīng)答報(bào)文后,之后進(jìn)入 FIN_WAIT_2 狀態(tài)。
- 等待服務(wù)端處理完數(shù)據(jù)后,也向客戶端發(fā)送 FIN 報(bào)文,之后服務(wù)端進(jìn)入 LAST_ACK 狀態(tài)。
- 客戶端收到服務(wù)端的 FIN 報(bào)文后,回一個(gè) ACK 應(yīng)答報(bào)文,之后進(jìn)入 TIME_WAIT 狀態(tài)
- 服務(wù)端收到了 ACK 應(yīng)答報(bào)文后,就進(jìn)入了 CLOSE 狀態(tài),至此服務(wù)端已經(jīng)完成連接的關(guān)閉。
- 客戶端在經(jīng)過 2MSL 一段時(shí)間后,自動(dòng)進(jìn)入 CLOSE 狀態(tài),至此客戶端也完成連接的關(guān)閉。
為什么要三次握手、四次揮手?
三次握手的原因:
- 三次握手可以阻止重復(fù)歷史連接的初始化
- 三次握手可以同步雙方的初始序列號
- 三次握手可以避免資源浪費(fèi)
四次次揮手的原因:
- 服務(wù)端通常需要等待完成數(shù)據(jù)的發(fā)送和處理,所以服務(wù)端的 ACK 和 FIN 一般都會(huì)分開發(fā)送,因此是需要四次揮手。
LINUX有哪些IO機(jī)制?
- 阻塞IO(Blocking IO):應(yīng)用程序在進(jìn)行IO操作時(shí),會(huì)一直阻塞等待IO完成,期間無法進(jìn)行其他操作。
- 非阻塞IO(Non-blocking IO):應(yīng)用程序在進(jìn)行IO操作時(shí),會(huì)立即返回,無論IO操作是否完成,應(yīng)用程序都可以進(jìn)行其他操作。需要通過輪詢的方式來判斷IO是否完成,因此效率較低。
- IO多路復(fù)用(IO Multiplexing):通過select、poll、epoll等系統(tǒng)調(diào)用,在一個(gè)進(jìn)程中可以同時(shí)監(jiān)控多個(gè)文件描述符,當(dāng)有任何一個(gè)文件描述符就緒時(shí),就可以進(jìn)行IO操作。
- 信號驅(qū)動(dòng)IO(Signal Driven IO):應(yīng)用程序在進(jìn)行IO操作時(shí),向內(nèi)核注冊一個(gè)信號處理函數(shù),內(nèi)核在IO完成時(shí)會(huì)向應(yīng)用程序發(fā)送一個(gè)信號,應(yīng)用程序收到信號后再進(jìn)行數(shù)據(jù)處理。
- 異步IO(Asynchronous IO):應(yīng)用程序進(jìn)行IO操作時(shí),可以立即返回,內(nèi)核負(fù)責(zé)將數(shù)據(jù)讀取到指定的緩沖區(qū)中,并在完成后通知應(yīng)用程序,應(yīng)用程序可以繼續(xù)進(jìn)行其他操作。異步IO需要操作系統(tǒng)和硬件的支持,目前主要應(yīng)用于高性能IO場景。
select poll epoll,底層實(shí)現(xiàn)有什么區(qū)別?
select 和 poll 并沒有本質(zhì)區(qū)別,它們內(nèi)部都是使用「線性結(jié)構(gòu)」來存儲(chǔ)進(jìn)程關(guān)注的 Socket 集合。
在使用的時(shí)候,首先需要把關(guān)注的 Socket 集合通過 select/poll 系統(tǒng)調(diào)用從用戶態(tài)拷貝到內(nèi)核態(tài),然后由內(nèi)核檢測事件,當(dāng)有網(wǎng)絡(luò)事件產(chǎn)生時(shí),內(nèi)核需要遍歷進(jìn)程關(guān)注 Socket 集合,找到對應(yīng)的 Socket,并設(shè)置其狀態(tài)為可讀/可寫,然后把整個(gè) Socket 集合從內(nèi)核態(tài)拷貝到用戶態(tài),用戶態(tài)還要繼續(xù)遍歷整個(gè) Socket 集合找到可讀/可寫的 Socket,然后對其處理。
很明顯發(fā)現(xiàn),select 和 poll 的缺陷在于,當(dāng)客戶端越多,也就是 Socket 集合越大,Socket 集合的遍歷和拷貝會(huì)帶來很大的開銷,因此也很難應(yīng)對 C10K。
epoll 是解決 C10K 問題的利器,通過兩個(gè)方面解決了 select/poll 的問題。
- epoll 在內(nèi)核里使用「紅黑樹」來關(guān)注進(jìn)程所有待檢測的 Socket,紅黑樹是個(gè)高效的數(shù)據(jù)結(jié)構(gòu),增刪改一般時(shí)間復(fù)雜度是 O(logn),通過對這棵黑紅樹的管理,不需要像 select/poll 在每次操作時(shí)都傳入整個(gè) Socket 集合,減少了內(nèi)核和用戶空間大量的數(shù)據(jù)拷貝和內(nèi)存分配。
- epoll 使用事件驅(qū)動(dòng)的機(jī)制,內(nèi)核里維護(hù)了一個(gè)「鏈表」來記錄就緒事件,只將有事件發(fā)生的 Socket 集合傳遞給應(yīng)用程序,不需要像 select/poll 那樣輪詢掃描整個(gè)集合(包含有和無事件的 Socket ),大大提高了檢測的效率。
BIO 和 NIO 的區(qū)別?
Java中,BIO和NIO都是IO模型,它們的主要區(qū)別在于:
- 阻塞和非阻塞:BIO采用阻塞模式,即在進(jìn)行IO操作時(shí),線程會(huì)一直阻塞等待IO完成。而NIO采用非阻塞模式,即在進(jìn)行IO操作時(shí),線程會(huì)立即返回,無論IO操作是否完成,線程都可以進(jìn)行其他操作。
- IO模型:BIO采用同步阻塞IO模型,即一個(gè)線程只能處理一個(gè)連接,當(dāng)有大量連接時(shí),需要大量的線程來處理,會(huì)導(dǎo)致系統(tǒng)資源浪費(fèi)。NIO采用同步非阻塞IO模型,將客戶端發(fā)送的連接請求都會(huì)注冊到多路復(fù)用器上,這樣一個(gè)線程可以處理多個(gè)連接,當(dāng)有大量連接時(shí),只需要少量的線程來處理,可以有效地提高系統(tǒng)資源利用率。
算法
鏈表判斷相交
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
面試總結(jié)
感覺
非常有收獲的一次面試,值得之后自己單獨(dú)寫一寫文章去記錄一下?;A(chǔ)不牢地動(dòng)山搖,雖然都是基礎(chǔ)問題,但是往下深挖根莖葉脈全部相連,這些問題面試題里面都有解,但是自己真的是只知道表面,淺淺看個(gè)大概就上戰(zhàn)場了,還有就是非常感謝面試官,反問環(huán)節(jié)給了我很多建議,包括面試、策略、基礎(chǔ)、算法等等,是一次寶貴的學(xué)習(xí)經(jīng)歷,說不遺憾是假的,但我很開心,有所收獲就好。
不足之處
基礎(chǔ)不夠扎實(shí),很多只知其表不知其里,而且面試官經(jīng)驗(yàn)豐富,非常敏銳,當(dāng)時(shí)壓力也很大,被問倒了心態(tài)也不穩(wěn),有的可以答出來的也沒想起來怎么說。
沒有幾個(gè)答的特別好的,稍微說一點(diǎn)就會(huì)被問深直到不會(huì),這里就不貼當(dāng)時(shí)回答了,說的都很淺,下來每一個(gè)問題都要仔細(xì)研究,要將知識(shí)連接成網(wǎng)。
自己給自己構(gòu)造一個(gè)場景,順著把用到的技術(shù)全部梳理一遍,能講出這個(gè)整體過程,有頭有尾,才能真的理解,比如hashmap插入數(shù)據(jù)的過程、threadlocal創(chuàng)建釋放的過程、String怎么實(shí)現(xiàn)的字符串拼接、SpringBoot框架搭建的每一步等等。