不容錯過的Java高級面試題

又到跨年之際,想必在這一年技術成長頗多的猿友們爲備戰金三銀四而蠢蠢欲動了吧。工欲善其事必先利其器。停止無病呻吟和眼高手低,腳踏實地地狂刷面試題,offer拿到手軟不再是空談。帝都的雁爲大家彙總本人在今次找工作中遇到的面試題,希望可以幫到猿友。

(PS:博主本次找工作參加面試的知名企業有:有快手/字節/阿里/滴滴/boss直聘/攜程/獵聘/好未來/京東/美團/噹噹,最終也如願進入其中一家大廠;面試題基於Java全棧,參照個人簡歷的技術棧由淺至深詢問。故建議猿友們在簡歷上寫自己hold得住的技能,切莫畫蛇添足。以本人技術棧爲例,答案均爲本人的理解,僅供參考。)

一、設計模式

列舉常被問到的設計模式。

策略(阿里/快手/京東/獵聘)

問:在項目哪些地方使用到了策略設計模式。

答:重構訂單狀態變更邏輯。系統中訂單的狀態有好多個,每一個狀態對應一種業務邏輯,以前的代碼按照if() else if()分支去處理,代碼臃腫且冗餘。我定義訂單策略的抽象類抽取共同行爲和屬性,再將每個訂單狀態的業務邏輯封裝爲訂單策略實現類,並放入spring的IOC中;定義一個枚舉,將訂單的狀態和訂單策略實現類的beanID進行映射。最後定義策略上下文對象,用於交互即可。

 

單例(快手/boss直聘/滴滴)

問:手寫一個單例。

答:我一般爲了簡單,會直接寫餓漢式。但會向面試官闡述單例的一些實現方式和注意事項。

單例是指一個類在一個JVM中僅有一個實例對象。常見的實現方式有餓漢式、懶漢式、線程安全的懶漢式、雙重檢驗鎖+volatile、靜態內部類、枚舉等方式。反射和反序列化可以破壞單例,所以需要在單例的構造中再次判斷實例對象是否已創建,進而拋出異常進行規避。

 

代理(獵聘/字節/阿里)

問:動態代理的實現方式。

答:繼承目標類或實現目標類接口。

JDK動態代理基於實現目標類接口,寫一個方法增強器實現invocationHandler接口,重寫其invoke方法,然後通過Proxy.newInstance的方式傳入目標類的類加載器、目標類實現的接口,以及自定義的方法增強器,然後創建代理對象。本質上是動態拼接了一個實現了目標類接口的代理類的字符串,將這個字符串輸出至本地的一個Java文件中,再通過Java編譯器編譯,變爲class文件,類加載器將其加載至JVM內存,以反射的方式進行實例化使用。

CGLIB動態代理基於繼承目標類。寫一個MethodIntercepter的實現類,重寫其invokeSuper方法,然後通過Enhancer的方式進行創建使用。底層通過ASM字節碼技術生成了三個文件:代理類字節碼、目標類索引文件、代理類索引文件。索引文件中將當前類中方法名稱和參數列表類型組成簽名,按照hash生成一個下標,然後通過switch case的方式列舉,故而訪問起來會比反射的方式更快。

 

觀察者(boss直聘/攜程/滴滴/美團/噹噹)

問:觀察者的使用場景。

答:訂單發貨後需要給商家發短信和郵件。由於我們系統前期沒有引入MQ,所以採用異步線程去進行解耦。發貨可以看作一個事件,短信和郵件可以看作兩個觀察者。採用spring的Event事件通知去實現此功能。

 

模板方法(阿里/快手/京東)

問:項目中怎麼使用的模板方法。

答:系統中很多批量導入的業務代碼冗餘,且其大致操作流程相似,爲了便於維護,故而將批量導入的流程抽取至抽象類中,具體的行爲抽象交給不同的業務代碼實現即可。

 

題外話(京東/獵聘/好未來/滴滴/噹噹)

問:目前對於設計模式的使用分爲截然不同的兩個派系,大力擁護和強烈反對。如何看待設計模式?

答:設計模式有時可能不直觀,非本人編寫的代碼,閱讀起來有可能有些繞,不利於新手修改。但它方便擴展,且可以使業務代碼之間很好的解耦,對於源碼的閱讀也有很大幫助。

二、JAVA基礎

Java基礎包括JDK和spring體系、mybatis。

線程池(快手/字節/阿里/滴滴/boss直聘/攜程/獵聘/好未來/京東/美團/噹噹)

問:線程池的核心組件有哪些?爲什麼不推薦使用JDK自帶線程池?如何手寫一個線程池。

答:線程池核心參數有核心線程數(線程池啓動後,一直處於活躍狀態的線程)、隊列長度(核心線程都在處理任務時,新來的任務會放入此隊列中)、最大線程數(隊列滿了後,會再開啓一定數量的線程,即線程池最多創建的線程數量)、拒絕策略(隊列滿了,最大線程數的線程均在處理任務,此時還有任務投遞,則進行的動作)。

JDK自帶的線程池有四種:固定長度(隊列無界)、可緩存(最大線程數無界)、可定時(最大線程數無界)、單例(隊列無界)。由於其無界(隊列或最大線程數),可能會造成線程太多或任務太多帶來的OOM(out of memory)問題。我研究過定時線程池,內部維護了一個延時工作隊列(底層採用小堆頂的方式實現),任務攜帶時間間隔的參數,每當任務要執行時,會把當前系統時間+時間間隔後,把這個任務再次投遞至延時工作隊列中。

手寫線程池思路:定義阻塞隊列。創建核心線程數的線程,使其一直處於活躍狀態(死循環即可),當有任務需要執行時,從隊列中獲取任務執行。

 

併發包(快手/字節/阿里/滴滴/boss直聘/攜程/獵聘/好未來/京東/美團/噹噹)

問:sync和lock的區別。公平鎖與非公平鎖的區別。CAS有什麼問題?如何解決?

答:我個人認爲lock就是仿照sync的底層原理以Java代碼的方式實現了一次。

Sync內部維護了一個monitor對象,用於管理鎖的開銷。其內部有鎖池(搶鎖失敗的線程)、等待池(調用鎖wait方法的線程)、鎖持有線程、重入次數、競爭邏輯。

Sync存在鎖的膨脹(粗化)。偏向鎖:即一把鎖在一段時間內只被同一個線程持有,那麼當這個線程來訪問鎖時,不會對其進行加鎖操作,而是在鎖對象的對象頭markword中存放偏向鎖的信息,記錄當前線程ID,用於比較。輕量級鎖:一個線程持有一把鎖的時間非常短,那麼搶鎖失敗的線程不會直接阻塞,而是自旋等待(將鎖對象頭中偏向鎖信息拷貝至自己線程的工作內存中,以CAS的方式自旋)。重量級鎖:如果多次自旋失敗,則不會繼續這種消耗CPU資源的行爲,而是直接將其阻塞,等待鎖的釋放。

Lock鎖本質上是AQS的一層封裝。其內存也有狀態(記錄鎖重入次數)、阻塞雙向鏈表(搶鎖失敗的線程)、鎖持有線程、condition的單向等待鏈表(調用condtion.await方法的線程)。

AQS默認創建爲非公平鎖,當線程獲取鎖失敗時,會短暫自旋一次,然後將當前線程通過LockSupport的方式進行阻塞,然後封裝爲NODE節點,放入阻塞鏈表的末尾。當鎖釋放後,從阻塞鏈表的頭部取出一個節點,將線程喚醒,繼續搶奪鎖資源。由於期間可能出現非阻塞鏈表的線程參與鎖資源的爭奪,對排隊的線程不公平,所以爲非公平鎖。

公平鎖在非公平鎖的基礎上進行判斷,搶到鎖的線程如果不是阻塞鏈表頭部的線程,則將其阻塞,放入阻塞鏈表末尾排隊等待。

CAS自旋消耗CPU資源,且存在ABA問題,可以通過版本號去解決。

參考:Synchronized的花花腸子AQS的傀儡之Lock鎖

 

集合源碼(快手/字節/阿里/boss直聘/攜程/獵聘/京東)

問:arrayList底層實現。HashMap1.7和1.8有什麼區別。ConcurrentHashMap1.7和1.8有什麼區別。

答:arrayList底層採用數組實現,默認長度10,1.5倍擴容。擴容需要數組的拷貝,刪除需要移位,由於共享全局的數組,且其對數組的維護沒有加鎖,故非線程安全。存在fail-fast機制。

HashMap1.7採用數組+單向鏈表實現,初始容量16,負載因子0.75,允許存放key爲空的數據(放在下標爲0處),將key的hashcode值通過hash算法得出hash值,再將hash值與數組的長度按位與,得出下標,在下標處以頭插法存放數據。由於沒有對共享的數組加鎖,非線程安全。2倍擴容,高併發下,由於頭插法和非線程安全,可能導致鏈表死循環。HashTable是在HashMap的基礎上,對其所有方法加sync保證線程安全,但HashTable不允許出現空值。

HashMap1.8採用數組+單向鏈表+紅黑樹實現。當單條鏈表的長度大於8且集合元素超過64,會將這條鏈表轉爲紅黑樹,提升查詢效率(鏈表時間複雜度爲O(n),紅黑樹時間複雜度爲O(log(n))。鏈表的插入方式改爲尾插法。

ConcurrentHashMap1.7底層採用16個segment對象,segment和hashTable類似,所以ConcurrentHashMap1.7保證線程安全的方式採用分段鎖,從而提升效率。

ConcurrentHashMap1.8是在HashMap1.8基礎上優化,對其所有非線程安全的操作加鎖處理。比如數組初始化和擴容,採用CAS方式保證線程安全,對於單個下標下數據的存放,採用sync鎖起來,粒度更細。且當某個線程訪問ConcurrentHashMap時,如果發現正在擴容,不會阻塞這個線程,而是幫助ConcurrentHashMap完成擴容。

 

Java內存結構(快手/阿里/獵聘/好未來/京東/噹噹)

問:類加載過程。雙親委派如何打破。Java內存如何劃分。常用的垃圾回收器和垃圾回收算法有哪些。

答:類加載器通過編譯、鏈接(驗證、準備、解析)、初始化的操作將class文件加載至Java內存中。

雙親委派是指Java查找類的方式自上而下(啓動類加載器、擴展類加載器、應用類加載器、自定義類加載器)。類加載器中有個findClass方法,遞歸向上查找上級類加載器,我們只需要繞過這個方法即可。

Java內存機構分爲:

線程獨佔:棧(由棧幀組成,每一個棧幀就是一個方法。棧幀由局部變量表、操作數棧、動態鏈接、返回地址組成)、程序計數器(記錄當前線程運行的位置)和本地方法棧(被native修飾,與C通訊)。

線程共享:元空間(存放靜態信息,類的字節碼信息)、堆(創建的對象、字符串常量池)。

堆內存:以分代算法劃分爲新生代和老年代。新生代又分爲eden區(剛創建的對象)、from和to區(複製算法,用於年齡累加)。

常用垃圾回收算法有:標記清除、標記整理、複製、GCROOT。

常用垃圾回收器有:CMS、G1、Servier New/Servier Old

參考:被解刨的JVM

 

Java內存模型和volatile(快手/阿里/滴滴/攜程/獵聘/京東/噹噹)

問:介紹下Java內存模型。Volatile如何保證內存可見的。

答:Java內存模型分爲主內存和工作內存(本地內存)。主內存存放全局共享變量數據,而工作內存存放這些共享變量的副本數據。每一個線程都會開闢自己的工作內存。而Volatile修飾的變量通過MESI緩存一致性協議去保證一致性,即當變量修改,CPU總線嗅探機制會捕獲到它的變動,將其內容刷新至主內存,然後將其它工作內存的變量值置爲無效,使得其它工作內存變量的值重新從主內存獲取此變量值,保證一致。

參考:volatile與JMM的那些恩怨情仇

 

mybatis組件(快手/阿里)

問:介紹下Mybatis的原理。

答:詳情參考通俗易懂的Mybatis工作原理

 

spring組件(快手/字節/阿里/獵聘/好未來/京東/噹噹)

問:介紹下bean的生命週期。AOP如何實現的。如何解決循環依賴問題。聲明式事務嵌套會發生什麼問題。

答:spring容器啓動時,會初始化spring的各種組件:beanFactory、後置處理器、event與listener的綁定、初始化單例對象等。初始化單例對象時,反射其無參構造創建對象,然後進行屬性賦值,檢查aware的依賴信息,執行後置處理器的前置操作,執行自定義init方法,執行後置處理器後置處理。

Spring通過三級緩存來解決單例的循環依賴,多例需要我們通過@primy或@qualify手動聲明。

AOP也是後置處理器的一種特殊實現。在後置處理中判斷當前類是否實現接口,進而判斷使用JDK動態代理還是CGLIB動態代理,在對應的invoke中,通過責任鏈+遞歸的方式去依次執行切面的通知,最後執行目標方法。

事務是一種特殊的通知,通過手動try catch來提交或回滾事務。

當同類的事務嵌套時,this會使其失效。不同類事務嵌套,按照事務傳播機制進行判斷是否回滾。

 

springMVC組件(快手/阿里)

問:攔截器的原理。

答:springMVC的核心類DispatherServlet的doDispath方法中爲springMVC的執行原理,通過模版方法獲取所有的攔截器,循環調用執行其三個抽象方法的實現。

三、主流技術

以簡歷爲例。

Redis(快手/字節/阿里/滴滴/boss直聘/攜程/獵聘/好未來/京東/美團/噹噹)

問:redis的數據結構。淘汰策略。持久化機制。集羣方式。分佈式鎖。分片原理。緩存擊穿/穿透/雪崩的原因和解決方案。

答:參考程序猿必備的Redis常見功能知識點,這些你都會嗎?

 

Zookeeper(滴滴/京東/噹噹)

問:ZK節點的類型。分佈式鎖原理。集羣原理。Base和CAP的理論。

答:節點類型分爲有序臨時、無序臨時、有序持久和無序持久。支持權限認證,有着強大的事件通知功能。

分佈式鎖分爲臨時節點和有序臨時節點。前者存在羊羣效應,後者類似於Java的lock公平鎖。

集羣要保證過半機制,通過MVCC和myid來選取事務ID最大、性能最好的節點當選leader,其數據同步遵循最終一致性(BASE),保證CAP中的CP,通過過半機制保證腦裂現象。

 

消息隊列(快手/阿里/滴滴/獵聘/好未來/京東/美團/噹噹)

問:MQ的工作方式有哪些。如何確保消息不丟失。如何解決重複消費。如何解決順序消費。基於mq解決分佈式事務。

答:工作方式有點對點、能者多勞(消費者手動ACK)、發佈訂閱fanout、路由direct、模糊路由topic。

消息不丟失:生產者投遞消息成功後,採用confirm確保投遞成功;消費者消費消息後,手動ACK保證消費成功;mq通過持久化機制將消息持久化至磁盤。

重複消費:記錄消息ID(雪花算法生成),消費消息前主動查詢比對。

順序消費:多個消息通過key保證其落在同一臺broker節點,然後使其被一個消費者消費。消費者在自己本地可採用內存隊列的方式提升消費效率。

分佈式事務:阿里的rocketMQ支持事務消息,通過兩端提交協議去保證事務參與者的事務提交或回滾的一致性。

 

微服務(快手/字節/阿里/滴滴/攜程/好未來/京東/美團)

問:什麼是服務治理。服務註冊和發現的過程。網關的工作原理。如何做到本地負載均衡。服務熔斷器的原理。

答:服務治理就是管理服務之間調用混亂的現象。註冊中心統一管理服務地址,其他服務啓動時將自身的信息以服務名稱的鍵值對投遞註冊至註冊中心,若想要獲取其它服務地址,也是通過其它服務名稱去註冊中心查找到集羣地址,然後本地通過負載均衡算法進行輪詢。

熔斷器可以實現服務降級、熔斷和線程隔離等功能。當此接口被熔斷器保護時,只要請求時長超過預設時間,就會走指定的方法進行響應,實現降級處理。熔斷是指如果請求此接口的線程數太多,則會走服務降級處理。我們也可以對熱點接口實現線程池隔離的方式提升性能,不同的接口採用不同的線程池處理。

 

Netty(字節/阿里/boss直聘/攜程/獵聘/好未來/京東/美團)

問:netty的作用。什麼是NIO多路複用。爲什麼NIO在LINUX比WINDOWS性能要好。

答:netty本質上就是對NIO的多路複用做了一層包裝。IO模型中分爲BIO(獲取不到就阻塞),NIO(獲取不到不阻塞)和AIO(異步)。爲了提升性能和降低CPU使用,採用一個線程統一管理這些socket的IO,Windows中才selector去不停的循環這些IO,但IO不一定每次循環都有數據,故會出現空輪詢的情況;而linux採用epoll的事件驅動回調,當socket的IO有數據時主動通知機制去獲取數據,故效率更高。

四、數據庫

主要是mysql。

數據結構(快手/字節/阿里/滴滴/boss直聘/攜程/獵聘/好未來/京東/美團)

問:MySQL的索引使用什麼數據結構。爲什麼使用B+樹。

答:mysql索引支持B樹、B+樹、Hash。默認採用B+樹,hash查詢快,但對範圍查詢不友好,B樹的非葉子節點存放了索引值和數據(或數據地址),導致其單個節點存放的索引值變少,樹的高度提升,增加IO次數。B+樹非葉子節點只存放索引值,故而樹的高度小,IO次數少,且葉子節點是一條有序的鏈表,對範圍查詢友好。

參考:mysql定位和優化慢查詢的方案

 

底層原理(滴滴/攜程)

問:mysql的redolog、undolog和binlog分別有什麼作用。

答:mysql默認採用innodb作爲存儲引擎,innodb維護了一個buffer pool的緩衝池來緩存數據。當更新數據時,會先將數據通過隨機IO的方式從磁盤讀取至緩衝池,在更新緩衝池數據前,會將緩衝池的數據寫入至undolog中,用於事務回滾;更新完緩衝池數據後,將緩衝池的數據寫入redolog中,用於災備的恢復;然後將緩衝池的數據同步至物理磁盤,並記錄在binlog日誌,用於做主從複製或與mq同步。

參考:mysql查詢和修改的底層原理

 

索引(快手/字節/阿里/滴滴/獵聘/好未來/京東/美團/噹噹)

問:爲什麼索引可以提高查詢效率。聚簇索引和非聚簇索引的區別。聯合索引的使用方式。

答:數據是按照索引的方式有序存放,如同我們的新華字典存放漢字一樣。聚餐索引即主鍵索引,數據和索引存放在一個文件中,而非聚簇索引的葉子節點存放的是主鍵的ID。聯合索引遵循最左原則。

參考:mysql定位和優化慢查詢的方案

 

事務隔離級別(快手/阿里)

問:事務隔離級別有哪些。

答:讀未提交(一個事務讀到另一個事務未提交的數據,有髒讀現象)、讀已提交(一個事務讀到另一個事務已經提交的數據,有不可重複度現象)、可重複讀(事務之間通過MVCC隔離,但有幻讀現象)、串行化(單線程)。

參考:mysql事務隔離級別以及MVCC的底層原理

 

sql優化(快手/字節/阿里/滴滴/獵聘/好未來/京東/美團/噹噹)

問:項目中如何優化sql,以什麼爲標準,需要注意什麼。

答:以阿里開發手冊爲準,通過explain去輸出sql的執行計劃,將其type優化爲range級別以上(至少爲range),同時避免filesort的內存排序情況。

參考:mysql定位和優化慢查詢的方案

 

歡迎大家和帝都的雁積極互動,頭腦交流會比個人埋頭苦學更有效!共勉!

公衆號:帝都的雁

 

 

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