對線面試官 | 位元組跳動一面
小夥伴們,大家新年好,今天給大家分享位元組跳動抖音電商的面經,希望對小夥伴們有所幫助~
面試官:你好,我是位元組跳動的面試官xxx,請問是大彬嗎?
大彬:面試官,您好,我是大彬
面試官:現在即食麵試嗎?
大彬:嗯嗯,可以的
面試官:那我們現在開始面試吧
面試官:看你簡歷上寫了熟悉集合相關內容,你了解HashMap嗎?講一下HashMap的put方法?
獨白:果然一上來就是HashMap…
大彬:HashMap 實現了Map接口,用於保存鍵值對映射。其底層是使用數組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現的。
大彬:它的put方法過程如下:
- 如果table沒有初始化就先進行初始化過程
- 使用hash算法計算key的索引
- 判斷索引處有沒有存在元素,沒有就直接插入
- 如果索引處存在元素,則遍歷插入,有兩種情況,一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入
- 鏈表的數量大於閾值8,就要轉換成紅黑樹的結構
- 添加成功後會檢查是否需要擴容
面試官:嗯,剛剛你提到HashMap的擴容,詳細講一下?
獨白:emm,給自己挖坑了…
大彬:以JDK1.8為例,當往HashMap放入元素時,如果元素個數大於threshold時,會進行擴容,使用2倍容量的數組代替原有數組。
大彬:由於數組的容量是以2的冪次方擴容的,那麼一個Entity在擴容時,新的位置要麼在原位置,要麼在原長度+原位置的位置。
大彬:原因是數組長度變為原來的2倍,表現在二進制上就是多了一個高位參與數組下標計算。
大彬:也就是說,在元素拷貝過程不需要重新計算元素在數組中的位置,只需要看看原來的hash值新增的那個bit是1還是0,是0的話索引沒變,是1的話索引變成「原索引+oldCap」(根據e.hash & (oldCap - 1) == 0
判斷) 。
大彬:這樣可以省去重新計算hash值的時間,而且由於新增的1bit是0還是1可以認為是隨機的,因此resize的過程會均勻的把之前的衝突的節點分散到新的bucket。
面試官:小夥子,基礎還算不錯。看你簡歷上寫了精通MySQL,來講講一下MySQL的索引結構?
獨白:卧槽,以後再也不敢寫精通了….還好昨天背了大廠面試手冊,現在一點都不慌,需要的可以到公眾號【程序員大彬】後台回復【手冊】獲取大廠面試資料
大彬:MySQL 數據庫使用最多的索引類型是BTREE索引,底層基於B+樹數據結構來實現。
大彬:B+ 樹是基於B 樹和葉子節點順序訪問指針進行實現,它具有B樹的平衡性,並且通過順序訪問指針來提高區間查詢的性能。
大彬:進行查找操作時,首先在根節點進行二分查找,找到key所在的指針,然後遞歸地在指針所指向的節點進行查找。直到查找到葉子節點,然後在葉子節點上進行二分查找,找出 key 所對應的數據項。
面試官:為什麼索引要用B+樹來實現呢,而不是用二叉樹?
大彬:B+樹有個特點,就是夠矮夠胖,能有效地減少訪問節點次數從而提高性能。
大彬:雖然二叉樹也有很好的查找性能log2N,但是當N比較大的時候,樹的深度比較高。數據查詢的時間主要依賴於磁盤IO的次數,二叉樹深度越大,查找的次數越多,性能越差。最壞的情況會退化成鏈表。所以,B+樹更適合作為MySQL索引結構。
面試官:那又為什麼不用B樹呢?
獨白:現在面試也太卷了趴,這是要造火箭啊…
大彬:因為B樹的分支結點存儲着數據,我們要找到具體的數據,需要進行一次中序遍歷按序來掃。而由於B+樹的數據都存儲在葉子結點中,葉子結點均為索引,方便掃庫,只需要掃一遍葉子結點即可。所以B+樹更加適合在區間查詢的情況,而在數據庫中基於範圍的查詢是非常頻繁的,所以B+樹更適合用於數據庫索引。
面試官:知道聚集索引嗎?
大彬:聚集索引嚴格來說並不是索引類型,而是一種數據存儲方式,具體細節依賴於其實現方式。如innodb聚集索引的葉子節點存放了整張表的行記錄。
大彬:聚集索引類似字典的拼音目錄。表中的數據按照聚集索引的規則來存儲的。就像新華字典,整本字典是按照A-Z的順序來排列。這也是一個表只能有一個聚集索引的原因。
面試官:那聚簇索引相比非聚簇索引有什麼優點?
大彬:1. 數據訪問更快,因為聚簇索引將索引和數據保存在同一個B+樹中,因此從聚簇索引中獲取數據比非聚簇索引更快。
大彬:2. 聚集索引葉子節點的存儲是邏輯上連續的,所以對於主鍵的排序查找和範圍查找速度會更快。
面試官:嗯,問點其他的,線程池知道吧?
大彬:線程池,顧名思義,就是一個管理線程的池子。
面試官:那為什麼使用線程池呢?
大彬:之所以使用線程池,主要有三點原因:
- 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷毀造成的消耗。
- 提高響應速度。當任務到達時,可以不需要等到線程創建就能立即執行。
- 提高線程的可管理性。統一管理線程,避免系統創建大量同類線程而導致消耗完內存。
面試官:嗯,那你講一下線程池的幾個參數?
獨白:老八股文了嘿嘿~
大彬:先來看看ThreadPoolExecutor 的通用構造函數:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
大彬:其中有7個參數。分別是corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler
大彬:corePoolSize。當有新任務時,如果線程池中線程數沒有達到線程池的基本大小,則會創建新的線程執行任務,否則將任務放入阻塞隊列。當線程池中存活的線程數總是大於 corePoolSize 時,應該考慮調大 corePoolSize。
大彬:maximumPoolSize。當阻塞隊列填滿時,如果線程池中線程數沒有超過最大線程數,則會創建新的線程運行任務。否則根據拒絕策略處理新任務。非核心線程類似於臨時借來的資源,這些線程在空閑時間超過 keepAliveTime 之後,就應該退出,避免資源浪費。
大彬:BlockingQueue。存儲等待運行的任務。
大彬:keepAliveTime。非核心線程空閑後,保持存活的時間,此參數只對非核心線程有效。設置為0,表示多餘的空閑線程會被立即終止。
大彬:TimeUnit。時間單位,具體如下:
TimeUnit.DAYS
TimeUnit.HOURS
TimeUnit.MINUTES
TimeUnit.SECONDS
TimeUnit.MILLISECONDS
TimeUnit.MICROSECONDS
TimeUnit.NANOSECONDS
大彬:ThreadFactory。每當線程池創建一個新的線程時,都是通過線程工廠方法來完成的。在 ThreadFactory 中只定義了一個方法 newThread,每當線程池需要創建新線程就會調用它。
public class MyThreadFactory implements ThreadFactory {
private final String poolName;
public MyThreadFactory(String poolName) {
this.poolName = poolName;
}
public Thread newThread(Runnable runnable) {
return new MyAppThread(runnable, poolName);//將線程池名字傳遞給構造函數,用於區分不同線程池的線程
}
}
大彬:RejectedExecutionHandler。當隊列和線程池都滿了時,根據拒絕策略處理新任務。
AbortPolicy:默認的策略,直接拋出RejectedExecutionException
DiscardPolicy:不處理,直接丟棄
DiscardOldestPolicy:將等待隊列隊首的任務丟棄,並執行當前任務
CallerRunsPolicy:由調用線程處理該任務
面試官:好的。你了解 Spring AOP 嗎?
大彬:AOP,其實就是面向切面編程,將一些公共邏輯(事務管理、日誌、緩存等)封裝成切面,跟業務代碼進行分離,可以減少系統的重複代碼和降低模塊之間的耦合度。切面就是那些與業務無關,但所有業務模塊都會調用的公共邏輯。
大彬:Spring AOP是通過動態代理技術實現的。
面試官:哦,那動態代理的實現方式有哪些?
大彬:動態代理技術的實現方式有兩種:
-
基於接口的 JDK 動態代理。
-
基於繼承的 CGLib 動態代理。在Spring中,如果目標類沒有實現接口,那麼Spring AOP會選擇使用CGLIB來動態代理目標類。
面試官:你剛剛提到CGlib動態代理,能詳細介紹下嗎?
大彬:CGLIB,就是Code Generator Library,它是一個強大的、高性能的代碼生成庫,被廣泛應用於AOP框架中,用以提供方法攔截操作。
大彬:CGLIB代理主要通過對位元組碼的操作,為對象引入間接級別,以控制對象的訪問。
大彬:CGLib 動態代理相對於 JDK 動態代理局限性就小很多,目標對象不需要實現接口,底層是通過繼承目標對象產生代理子對象。
面試官:不錯,好好準備二面吧~
碼字不易,如果覺得對你有幫助,可以點個贊鼓勵一下!