真等不及了,沖阿里去了!
大家好,我是小林。
阿里巴巴上周開啟了 25 屆的實習招聘,阿里是Java 一線大廠,到底面試難度如何呢?同學們準備好了嗎?
今天分享一位校招同學阿里的 Java 后端開發(fā)的面經(jīng),阿里的風格會比較關注 Java 和后端組件,而網(wǎng)絡和系統(tǒng)考察通常都比較少的,所以準備面阿里的同學 Java 和后端組件這兩方面的要準備好。
這次的 Java 后端開發(fā)的面經(jīng),考察的知識點, 針對八股文的涉及的內(nèi)容,我?guī)痛蠹伊辛艘幌拢?/p>
- Java 基礎:面向?qū)ο?、多態(tài)、重載
- Java 集合:HashMap 連環(huán)問、紅黑樹
- Java并發(fā):volatile、Synchronized、ReentrantLock
- Java 線程池:線程池參數(shù)、核心線程數(shù)設置
- MySQL:索引結構、建立索引規(guī)則、explain、聯(lián)合索引
八股這塊一共問了 30 分鐘,其余時間問了項目方面的內(nèi)容。
Java基礎
講一下Java面向?qū)ο蟮奶攸c
封裝、繼承、多態(tài)是Java面向?qū)ο缶幊痰娜筇攸c。
- 封裝(Encapsulation):封裝是面向?qū)ο缶幊痰幕咎攸c之一,它將數(shù)據(jù)和方法封裝在對象內(nèi)部,隱藏對象的內(nèi)部實現(xiàn)細節(jié),只暴露必要的接口供外部訪問。通過封裝,可以實現(xiàn)信息的隱藏和保護,提高代碼的安全性和可靠性。
- 繼承(Inheritance):繼承是面向?qū)ο缶幊痰闹匾攸c,它允許一個類(子類)繼承另一個類(父類)的屬性和方法。子類可以重用父類的代碼,并可以通過擴展和重寫來增加新的功能或修改現(xiàn)有功能。繼承提高了代碼的復用性和可維護性,同時也體現(xiàn)了類與類之間的關系。
- 多態(tài)(Polymorphism):多態(tài)是面向?qū)ο缶幊痰暮诵母拍钪?,它允許不同對象對同一消息作出不同的響應。在Java中,多態(tài)性通過方法重載和方法重寫來實現(xiàn)。方法重載是指在同一個類中可以定義多個同名方法,但參數(shù)列表不同;方法重寫是指子類可以重寫父類的方法,實現(xiàn)不同的行為。多態(tài)性提高了代碼的靈活性和擴展性,使得程序更易于理解和維護。
用過“多態(tài)”嗎?舉一個具體例子
一個具體的例子是,假設有一個動物類(Animal)和它的兩個子類:狗類(Dog)和貓類(Cat)。它們都有一個名為“makeSound”的方法,但是每種動物發(fā)出的聲音是不同的。
class Animal {
public void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
public void makeSound() {
System.out.println("Woof");
}
}
class Cat extends Animal {
public void makeSound() {
System.out.println("Meow");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
dog.makeSound(); // 輸出:Woof
cat.makeSound(); // 輸出:Meow
}
}
通過多態(tài)性,可以創(chuàng)建一個Animal類型的引用指向一個具體的Dog或Cat對象。當調(diào)用這個引用的“makeSound”方法時,根據(jù)實際指向的對象類型,會執(zhí)行相應子類的方法,從而實現(xiàn)不同動物發(fā)出不同聲音的效果。這樣就體現(xiàn)了多態(tài)的特性,同一個方法調(diào)用可以產(chǎn)生不同的行為,提高了代碼的靈活性和可擴展性。
多態(tài)和重載有什么關系?
重載是一種編譯時多態(tài),而多態(tài)是一種運行時多態(tài)。兩者都是實現(xiàn)多態(tài)性的方式,但發(fā)生的時間點和機制不同。
- 重載是指在同一個類中,方法名相同但參數(shù)列表不同的情況,通過參數(shù)個數(shù)、類型或順序的不同來區(qū)分不同的方法。重載是靜態(tài)綁定的概念,編譯器在編譯期間根據(jù)方法的參數(shù)列表來確定調(diào)用哪個方法。
- 多態(tài)是指同一個方法名可以在不同的類中有不同的實現(xiàn),不同的子類可以重寫父類的方法,通過父類引用指向子類對象時,根據(jù)實際對象的類型來確定調(diào)用哪個方法。多態(tài)是動態(tài)綁定的概念,運行時根據(jù)對象的實際類型來確定調(diào)用哪個方法。
Java集合
Java中的HashMap了解嗎?
了解的,HashMap是Java中常用的一種數(shù)據(jù)結構,它基于哈希表實現(xiàn),用于存儲鍵值對
聊聊HashMap的底層結構
在 JDK 1.7 版本之前, HashMap 數(shù)據(jù)結構是數(shù)組和鏈表,HashMap通過哈希算法將元素的鍵(Key)映射到數(shù)組中的槽位(Bucket)。如果多個鍵映射到同一個槽位,它們會以鏈表的形式存儲在同一個槽位上,因為鏈表的查詢時間是O(n),所以沖突很嚴重,一個索引上的鏈表非常長,效率就很低了。
所以在 JDK 1.8 版本的時候做了優(yōu)化,當一個鏈表的長度超過8的時候就轉(zhuǎn)換數(shù)據(jù)結構,不再使用鏈表存儲,而是使用紅黑樹,查找時使用紅黑樹,時間復雜度O(log n),可以提高查詢性能,但是在數(shù)量較少時,即數(shù)量小于6時,會將紅黑樹轉(zhuǎn)換回鏈表。
圖片
為什么要引入紅黑樹,而不用其他樹?
- 為什么不使用二叉排序樹?問題主要出現(xiàn)在二叉排序樹在添加元素的時候極端情況下會出現(xiàn)線性結構。比如由于二叉排序樹左子樹所有節(jié)點的值均小于根節(jié)點的特點,如果我們添加的元素都比根節(jié)點小,會導致左子樹線性增長,這樣就失去了用樹型結構替換鏈表的初衷,導致查詢時間增長。所以這是不用二叉查找樹的原因。
- 為什么不使用平衡二叉樹呢?紅黑樹不追求"完全平衡",而而AVL是嚴格平衡樹,因此在增加或者刪除節(jié)點的時候,根據(jù)不同情況,旋轉(zhuǎn)的次數(shù)比紅黑樹要多。紅黑樹讀取略遜于AVL,維護強于AVL,空間開銷與AVL類似,內(nèi)容極多時略優(yōu)于AVL,維護優(yōu)于AVL。
基本上主要的幾種平衡樹看來,紅黑樹有著良好的穩(wěn)定性和完整的功能,性能表現(xiàn)也很不錯,綜合實力強,在諸如STL的場景中需要穩(wěn)定表現(xiàn)。
HashMap會出現(xiàn)紅黑樹一直增高變成無限高的情況嗎?
不能無限增長。當集合中的節(jié)點數(shù)超過了閾值,HashMap會進行擴容,這時原始的紅黑樹節(jié)點會被打散,可能會退化成鏈表結構。
HashMap讀和寫的時間復雜度是多少?
HashMap的讀?。ú檎遥┖蛯懭耄ú迦?、更新、刪除)操作的時間復雜度均為O(1),即常數(shù)時間復雜度。
這是因為HashMap內(nèi)部使用哈希表來存儲鍵值對,通過計算鍵的哈希值可以直接定位到對應的存儲位置,從而實現(xiàn)快速的讀取和寫入操作。
在理想情況下,HashMap可以在常數(shù)時間內(nèi)完成查找和插入操作,具有高效的性能表現(xiàn)。
HashMap是線程安全的嗎?怎么解決?
不是線程安全的。
- JDK 1.7 HashMap 采用數(shù)組 + 鏈表的數(shù)據(jù)結構,多線程背景下,在數(shù)組擴容的時候,存在 Entry 鏈死循環(huán)和數(shù)據(jù)丟失問題。
- JDK 1.8 HashMap 采用數(shù)組 + 鏈表 + 紅黑二叉樹的數(shù)據(jù)結構,優(yōu)化了 1.7 中數(shù)組擴容的方案,解決了 Entry 鏈死循環(huán)和數(shù)據(jù)丟失問題。但是多線程背景下,put 方法存在數(shù)據(jù)覆蓋的問題。
解決的方式:
- 使用ConcurrentHashMap:ConcurrentHashMap是Java提供的線程安全的哈希表實現(xiàn),它通過分段鎖(Segment)和CAS操作來保證線程安全性,適用于高并發(fā)環(huán)境。
- 使用Collections.synchronizedMap:可以通過Collections工具類中的synchronizedMap方法來創(chuàng)建一個線程安全的HashMap,該方法會返回一個同步的Map對象,但性能可能不如ConcurrentHashMap。
解決線程安全問題還有哪些辦法?
- 使用同步關鍵字synchronized:可以通過在方法或代碼塊上使用synchronized關鍵字來實現(xiàn)線程安全,確保同一時刻只有一個線程可以訪問共享資源。
- 使用volatile關鍵字:可以使用volatile關鍵字修飾變量,保證變量的可見性,即一個線程修改了變量的值,其他線程能立即看到最新值,從而避免數(shù)據(jù)不一致的問題。
- 使用線程安全的工具類:Java中提供了諸如AtomicInteger、AtomicLong、CountDownLatch等線程安全的工具類,可以幫助解決并發(fā)場景下的線程安全性問題。
- 使用并發(fā)容器:Java中提供了多種線程安全的并發(fā)容器,如ConcurrentLinkedQueue、CopyOnWriteArrayList等,可以替代傳統(tǒng)的非線程安全容器來解決多線程并發(fā)訪問問題。
Java并發(fā)
volatile關鍵字是如何保證內(nèi)存可見性的?底層是怎么實現(xiàn)的?
volatile關鍵字通過兩種機制來保證內(nèi)存可見性:
- 禁止指令重排序:在程序運行時,為了提高性能,編譯器和處理器可能會對指令進行重排序,這可能會導致變量的更新操作被延遲執(zhí)行或者亂序執(zhí)行,從而使得其他線程無法立即看到最新的值。使用volatile關鍵字修飾的變量會禁止指令重排序,保證變量的更新操作按照代碼順序執(zhí)行。
- 內(nèi)存屏障(Memory Barrier):在多核處理器架構下,每個線程都有自己的緩存,volatile關鍵字會在寫操作后插入寫屏障(Write Barrier),在讀操作前插入讀屏障(Read Barrier),確保變量的更新能夠立即被其他線程看到,保證內(nèi)存可見性。
通過禁止指令重排序和插入內(nèi)存屏障,volatile關鍵字能夠保證被修飾變量的更新操作對其他線程是可見的,從而有效解決了多線程環(huán)境下的內(nèi)存可見性問題。
為什么需要保證內(nèi)存可見性?
如果不保證內(nèi)存可見性,就會出現(xiàn)數(shù)據(jù)臟讀,一個線程修改了共享變量的值,但其他線程無法立即看到最新值,導致其他線程讀取到了過期數(shù)據(jù),從而產(chǎn)生錯誤的結果。
通過保證內(nèi)存可見性,避免數(shù)據(jù)不一致性和并發(fā)訪問帶來的問題,保證程序的正確性和穩(wěn)定性。
volatile為什么要禁止指令重排,能舉一個具體的指令重排出現(xiàn)問題的例子嗎
禁止指令重排是為了確保程序的執(zhí)行順序與代碼編寫順序一致,特別是在多線程環(huán)境下,避免出現(xiàn)意外的結果。具體來說,如果不禁止指令重排,可能會導致以下問題:
舉例來說,假設有如下代碼片段:
int a = 0;
boolean flag = false;
// 線程1
a = 1;
flag = true;
// 線程2
if (flag) {
System.out.println(a);
}
如果發(fā)生指令重排,可能會導致線程2在判斷flag時先于a的賦值操作,那么線程2就會打印出0,而不是預期的1。這種情況下,禁止指令重排可以確保線程2在看到flag為true時,也能看到a被正確賦值為1,避免出現(xiàn)問題。
因此,通過禁止指令重排,可以保證程序的執(zhí)行順序符合代碼邏輯,避免出現(xiàn)意外的行為,特別是在涉及多線程并發(fā)的情況下更為重要。
Synchronized的底層原理是什么?鎖升級的過程了解嗎?
- 底層實現(xiàn):Synchronized關鍵字底層是使用monitor對象鎖實現(xiàn)的,每一個對象關聯(lián)一個monitor對象,而monitor對象可以看成是一個對象鎖,它采用互斥的方式讓同一時刻至多只有一個線程能持有對象鎖,其他線程再想獲取這個對 象鎖時會被阻塞住,這樣就能保證擁有鎖的線程可以安全的執(zhí)行臨界區(qū)的代碼。
- 鎖升級:是指JVM根據(jù)鎖的競爭情況和對象的狀態(tài),將對象的鎖從偏向鎖、輕量級鎖升級為重量級鎖的過程。偏向鎖是指針對無競爭的情況下,鎖會偏向于第一個獲取鎖的線程;輕量級鎖是指針對短時間內(nèi)只有一個線程競爭鎖的情況下,使用CAS操作來避免阻塞;重量級鎖是指多個線程競爭同一個鎖時,通過操作系統(tǒng)的互斥量來實現(xiàn)線程阻塞和喚醒。鎖升級的過程是為了提高多線程并發(fā)訪問的效率和性能。
線程是怎么確定拿到鎖的?
線程確定拿到鎖的過程:是通過檢查鎖的狀態(tài)并嘗試獲取鎖來實現(xiàn)的。在JVM中,鎖信息具體是存放在Java對象頭中的。
當一個線程嘗試進入synchronized代碼塊或方法時,JVM會檢查對應對象的鎖狀態(tài)。如果對象的鎖未被其他線程持有,即鎖狀態(tài)為可獲取,那么該線程將成功獲取鎖并進入臨界區(qū)執(zhí)行代碼。
鎖信息具體放到哪的?
鎖的狀態(tài)信息是Java對象頭中的 Mark Word 字段,這個字段對象關于鎖的信息、垃圾回收信息等。
圖片
Java 對象在內(nèi)存中的表示
JVM通過操作對象的頭部信息來實現(xiàn)鎖的獲取、釋放以及等待隊列的管理。當線程成功獲取鎖后,對象的頭部信息會被更新為當前線程的標識,表示該線程擁有了這個鎖。
其他線程在嘗試獲取同一個鎖時,會檢查對象的頭部信息,如果鎖已經(jīng)被其他線程持有,它們將會被阻塞直到鎖被釋放。
Synchronized加鎖和ReentrantLock加鎖有什么區(qū)別?
synchronized 和 ReentrantLock 都是 Java 中提供的可重入鎖:
- 用法不同:synchronized 可用來修飾普通方法、靜態(tài)方法和代碼塊,而 ReentrantLock 只能用在代碼塊上。
- 獲取鎖和釋放鎖方式不同:synchronized 會自動加鎖和釋放鎖,當進入 synchronized 修飾的代碼塊之后會自動加鎖,當離開 synchronized 的代碼段之后會自動釋放鎖。而 ReentrantLock 需要手動加鎖和釋放鎖
- 鎖類型不同:synchronized 屬于非公平鎖,而 ReentrantLock 既可以是公平鎖也可以是非公平鎖。
- 響應中斷不同:ReentrantLock 可以響應中斷,解決死鎖的問題,而 synchronized 不能響應中斷。
- 底層實現(xiàn)不同:synchronized 是 JVM 層面通過監(jiān)視器實現(xiàn)的,而 ReentrantLock 是基于 AQS 實現(xiàn)的。
Java 線程池
線程池了解過嗎?有哪些核心參數(shù)?
了解過的,線程池是為了減少頻繁的創(chuàng)建線程和銷毀線程帶來的性能損耗。
線程池分為核心線程池,線程池的最大容量,還有等待任務的隊列,提交一個任務,如果核心線程沒有滿,就創(chuàng)建一個線程,如果滿了,就是會加入等待隊列,如果等待隊列滿了,就會增加線程,如果達到最大線程數(shù)量,如果都達到最大線程數(shù)量,就會按照一些丟棄的策略進行處理。
圖片
線程池的構造函數(shù)有7個參數(shù):
圖片
- corePoolSize:線程池核心線程數(shù)量。默認情況下,線程池中線程的數(shù)量如果 <= corePoolSize,那么即使這些線程處于空閑狀態(tài),那也不會被銷毀。
- maximumPoolSize:線程池中最多可容納的線程數(shù)量。當一個新任務交給線程池,如果此時線程池中有空閑的線程,就會直接執(zhí)行,如果沒有空閑的線程且當前線程池的線程數(shù)量小于corePoolSize,就會創(chuàng)建新的線程來執(zhí)行任務,否則就會將該任務加入到阻塞隊列中,如果阻塞隊列滿了,就會創(chuàng)建一個新線程,從阻塞隊列頭部取出一個任務來執(zhí)行,并將新任務加入到阻塞隊列末尾。如果當前線程池中線程的數(shù)量等于maximumPoolSize,就不會創(chuàng)建新線程,就會去執(zhí)行拒絕策略。
- keepAliveTime:當線程池中線程的數(shù)量大于corePoolSize,并且某個線程的空閑時間超過了keepAliveTime,那么這個線程就會被銷毀。
- unit:就是keepAliveTime時間的單位。
- workQueue:工作隊列。當沒有空閑的線程執(zhí)行新任務時,該任務就會被放入工作隊列中,等待執(zhí)行。
- threadFactory:線程工廠??梢杂脕斫o線程取名字等等
- handler:拒絕策略。當一個新任務交給線程池,如果此時線程池中有空閑的線程,就會直接執(zhí)行,如果沒有空閑的線程,就會將該任務加入到阻塞隊列中,如果阻塞隊列滿了,就會創(chuàng)建一個新線程,從阻塞隊列頭部取出一個任務來執(zhí)行,并將新任務加入到阻塞隊列末尾。如果當前線程池中線程的數(shù)量等于maximumPoolSize,就不會創(chuàng)建新線程,就會去執(zhí)行拒絕策略。
為什么核心線程滿了之后是先加入阻塞隊列而不是直接加到總線程?
- 線程池創(chuàng)建線程需要獲取mainlock這個全局鎖,會影響并發(fā)效率,所以使用阻塞隊列把第一步創(chuàng)建核心線程與第三步創(chuàng)建最大線程隔離開來,起一個緩沖的作用。
- 引入阻塞隊列,是為了在執(zhí)行execute()方法時,盡可能的避免獲取全局鎖。
核心線程數(shù)一般設置為多少?
假設機器有N個CPU:
- 如果是CPU密集型應用,則線程池大小設置為N+1,線程的應用場景:主要是復雜算法
- 如果是IO密集型應用,則線程池大小設置為2N+1,線程的應用場景:主要是:數(shù)據(jù)庫數(shù)據(jù)的交互,文件上傳下載,網(wǎng)絡數(shù)據(jù)傳輸?shù)鹊?/li>
對于同時有計算工作和IO工作的任務,應該考慮使用兩個線程池,一個處理計算任務,一個處理IO任務,分別對兩個線程池按照計算密集型和IO密集型來設置線程數(shù)。
IO密集型的線程數(shù)為什么一般設置為2N+1?
在IO密集型任務中,線程通常會因為IO操作而阻塞,此時可以讓其他線程繼續(xù)執(zhí)行,充分利用CPU資源。設置為2N+1可以保證在有多個線程阻塞時,仍有足夠的線程可以繼續(xù)執(zhí)行。
MySQL
聊聊MySQL的索引結構,為什么使用B+樹而不用B樹?
MySQL 默認的存儲引擎 InnoDB 采用的是 B+ 作為索引的數(shù)據(jù)結構。
圖片
原因有:
- B+ 樹的非葉子節(jié)點不存放實際的記錄數(shù)據(jù),僅存放索引,因此數(shù)據(jù)量相同的情況下,相比存儲即存索引又存記錄的 B 樹,B+樹的非葉子節(jié)點可以存放更多的索引,因此 B+ 樹可以比 B 樹更「矮胖」,查詢底層節(jié)點的磁盤 I/O次數(shù)會更少。
- B+ 樹有大量的冗余節(jié)點(所有非葉子節(jié)點都是冗余索引),這些冗余索引讓 B+ 樹在插入、刪除的效率都更高,比如刪除根節(jié)點的時候,不會像 B 樹那樣會發(fā)生復雜的樹的變化;
- B+ 樹葉子節(jié)點之間用鏈表連接了起來,有利于范圍查詢,而 B 樹要實現(xiàn)范圍查詢,因此只能通過樹的遍歷來完成范圍查詢,這會涉及多個節(jié)點的磁盤 I/O 操作,范圍查詢效率不如 B+ 樹。
你是怎么建立索引的?一般是建立哪些字段的索引呢?
索引最大的好處是提高查詢速度,我經(jīng)常針對下面場景來建立索引:
- 字段有唯一性限制的,比如商品編碼;
- 經(jīng)常用于 WHERE 查詢條件的字段,這樣能夠提高整個表的查詢速度,如果查詢條件不是一個字段,可以建立聯(lián)合索引。
- 經(jīng)常用于 GROUP BY 和 ORDER BY 的字段,這樣在查詢的時候就不需要再去做一次排序了,因為我們都已經(jīng)知道了建立索引之后在 B+Tree 中的記錄都是排序好的。
怎么確定語句是否走了索引?
可以通過 explian查看執(zhí)行計劃來確認。
圖片
img
對于執(zhí)行計劃,參數(shù)有:
- possible_keys 字段表示可能用到的索引;
- key 字段表示實際用的索引,如果這一項為 NULL,說明沒有使用索引;
- key_len 表示索引的長度;
- rows 表示掃描的數(shù)據(jù)行數(shù)。
- type 表示數(shù)據(jù)掃描類型,我們需要重點看這個。
如果 typy=all,代表沒有走索引,進行了全表掃描。如果 key 不為 null,說明用到了索引。
如果要建立聯(lián)合索引,字段的順序有什么需要注意嗎?
- 最左匹配原則:聯(lián)合索引遵循最左匹配原則,即在查詢時,只有按照索引字段的順序從最左邊開始連續(xù)使用索引字段,索引才會被使用。因此,根據(jù)最常用作查詢條件的字段放在聯(lián)合索引的最左邊,可以提高索引的利用率。
- 區(qū)分度高的字段放在前面:將區(qū)分度高的字段放在聯(lián)合索引的前面,可以減少索引的掃描范圍,提高查詢效率。