圖解學習網(wǎng)站:https://xiaolincoding.com
大家好,我是小林。
這個月看到不少同學在面 OPPO 公司的秋招,也有不少同學好奇 OPPO 面試會問什么?
OPPO 主營業(yè)務是手機,雖然公司業(yè)務偏向手機硬件業(yè)務偏多,但是也還是會有一些后端開發(fā)的崗位,面試風格也是八股+項目+算法,跟互聯(lián)網(wǎng)沒太大的區(qū)別。
OPPO 校招薪資還是很不錯的,跟互聯(lián)網(wǎng)公司基本差不多,去年 OPPO 校招薪資,普通檔的是 22k*15,整體上也接近 35w 年薪了。
這次,就來分享一位同學的 OPPO 校招后端開發(fā)的面經(jīng),考察了 Java 并發(fā)+MySQL+Redis+Kafka 這幾個方面的知識,整體面試總共面了 40 分鐘。
同學反饋面試體驗很不錯,面試官人還是不錯,會給時間思考,也有在引導回答,很細自己準備不充分,只能回答淺層的問題,深問一點就不會了。
大家覺得難度如何呢?
Java
Java中的線程狀態(tài)有哪些?
源自《Java并發(fā)編程藝術》 java.lang.Thread.State枚舉類中定義了六種線程的狀態(tài),可以調(diào)用線程Thread中的getState()方法獲取當前線程的狀態(tài)。
線程狀態(tài) | 解釋 |
---|---|
NEW | 尚未啟動的線程狀態(tài),即線程創(chuàng)建,還未調(diào)用start方法 |
RUNNABLE | 就緒狀態(tài)(調(diào)用start,等待調(diào)度)+正在運行 |
BLOCKED | 等待監(jiān)視器鎖時,陷入阻塞狀態(tài) |
WAITING | 等待狀態(tài)的線程正在等待另一線程執(zhí)行特定的操作(如notify) |
TIMED_WAITING | 具有指定等待時間的等待狀態(tài) |
TERMINATED | 線程完成執(zhí)行,終止狀態(tài) |
線程池的參數(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)建線程和銷毀線程帶來的性能損耗,線程池的工作原理如下圖:
線程池分為核心線程池,線程池的最大容量,還有等待任務的隊列,提交一個任務,如果核心線程沒有滿,就創(chuàng)建一個線程,如果滿了,就是會加入等待隊列,如果等待隊列滿了,就會增加線程,如果達到最大線程數(shù)量,如果都達到最大線程數(shù)量,就會按照一些丟棄的策略進行處理。
Java中的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)的
ReentrantLock怎么實現(xiàn)可重入?
ReentrantLock的實現(xiàn)基于隊列同步器(AbstractQueuedSynchronizer,后面簡稱AQS),ReentrantLock的可重入功能基于AQS的同步狀態(tài):state。
其原理大致為:當某一線程獲取鎖后,將state值+1,并記錄下當前持有鎖的線程,再有線程來獲取鎖時,判斷這個線程與持有鎖的線程是否是同一個線程,如果是,將state值再+1,如果不是,阻塞線程。
當線程釋放鎖時,將state值-1,當state值減為0時,表示當前線程徹底釋放了鎖,然后將記錄當前持有鎖的線程的那個字段設置為null,并喚醒其他線程,使其重新競爭鎖。
//?acquires的值是1
final?boolean?nonfairTryAcquire(int?acquires)?{
//?獲取當前線程
final?Thread?current?=?Thread.currentThread();
//?獲取state的值
int?c?=?getState();
//?如果state的值等于0,表示當前沒有線程持有鎖
//?嘗試將state的值改為1,如果修改成功,則成功獲取鎖,并設置當前線程為持有鎖的線程,返回true
if?(c?==?0)?{
????if?(compareAndSetState(0,?acquires))?{
????????setExclusiveOwnerThread(current);
????????return?true;
????}
}
????//?state的值不等于0,表示已經(jīng)有其他線程持有鎖
????//?判斷當前線程是否等于持有鎖的線程,如果等于,將state的值+1,并設置到state上,獲取鎖成功,返回true
????//?如果不是當前線程,獲取鎖失敗,返回false
else?if?(current?==?getExclusiveOwnerThread())?{
????int?nextc?=?c?+?acquires;
????if?(nextc?<?0)?//?overflow
????????throw?new?Error("Maximum?lock?count?exceeded");
????setState(nextc);
????return?true;
}
return?false;
}
AQS底層原理是什么?
AQS全稱為AbstractQueuedSynchronizer,是Java中的一個抽象類。AQS是一個用于構建鎖、同步器、協(xié)作工具類的工具類(框架)。
AQS核心思想是,如果被請求的共享資源空閑,那么就將當前請求資源的線程設置為有效的工作線程,將共享資源設置為鎖定狀態(tài);如果共享資源被占用,就需要一定的阻塞等待喚醒機制來保證鎖分配。這個機制主要用的是CLH隊列的變體實現(xiàn)的,將暫時獲取不到鎖的線程加入到隊列中。
CLH:Craig、Landin and Hagersten隊列,是單向鏈表,AQS中的隊列是CLH變體的虛擬雙向隊列(FIFO),AQS是通過將每條請求共享資源的線程封裝成一個節(jié)點來實現(xiàn)鎖的分配。
主要原理圖如下:
AQS使用一個Volatile的int類型的成員變量來表示同步狀態(tài),通過內(nèi)置的FIFO隊列來完成資源獲取的排隊工作,通過CAS完成對State值的修改。
AQS廣泛用于控制并發(fā)流程的類,如下圖:
其中Sync
是這些類中都有的內(nèi)部類,其結構如下:
可以看到:Sync
是AQS
的實現(xiàn)。AQS
主要完成的任務:
- 同步狀態(tài)(比如說計數(shù)器)的原子性管理;線程的阻塞和解除阻塞;隊列的管理。
AQS原理
AQS最核心的就是三大部分:
- 狀態(tài):state;控制線程搶鎖和配合的FIFO隊列(雙向鏈表);期望協(xié)作工具類去實現(xiàn)的獲取/釋放等重要方法(重寫)。
狀態(tài)state
- 這里state的具體含義,會根據(jù)具體實現(xiàn)類的不同而不同:比如在Semapore里,他表示剩余許可證的數(shù)量;在CountDownLatch里,它表示還需要倒數(shù)的數(shù)量;在ReentrantLock中,state用來表示“鎖”的占有情況,包括可重入計數(shù),當state的值為0的時候,標識該Lock不被任何線程所占有。state是volatile修飾的,并被并發(fā)修改,所以修改state的方法都需要保證線程安全,比如getState、setState以及compareAndSetState操作來讀取和更新這個狀態(tài)。這些方法都依賴于unsafe類。
FIFO隊列
- 這個隊列用來存放“等待的線程,AQS就是“排隊管理器”,當多個線程爭用同一把鎖時,必須有排隊機制將那些沒能拿到鎖的線程串在一起。當鎖釋放時,鎖管理器就會挑選一個合適的線程來占有這個剛剛釋放的鎖。AQS會維護一個等待的線程隊列,把線程都放到這個隊列里,這個隊列是雙向鏈表形式。
實現(xiàn)獲取/釋放等方法
-
- 這里的獲取和釋放方法,是利用AQS的協(xié)作工具類里最重要的方法,是由協(xié)作類自己去實現(xiàn)的,并且含義各不相同;獲取方法:獲取操作會以來state變量,經(jīng)常會阻塞(比如獲取不到鎖的時候)。在Semaphore中,獲取就是acquire方法,作用是獲取一個許可證;而在CountDownLatch里面,獲取就是await方法,作用是等待,直到倒數(shù)結束;釋放方法:在Semaphore中,釋放就是release方法,作用是釋放一個許可證;在CountDownLatch里面,獲取就是countDown方法,作用是將倒數(shù)的數(shù)減一;需要每個實現(xiàn)類重寫tryAcquire和tryRelease等方法。
MySQL
MySQL聚簇索引和非聚簇索引的區(qū)別?
數(shù)據(jù)存儲:在聚簇索引中,數(shù)據(jù)行按照索引鍵值的順序存儲,也就是說,索引的葉子節(jié)點包含了實際的數(shù)據(jù)行。這意味著索引結構本身就是數(shù)據(jù)的物理存儲結構。非聚簇索引的葉子節(jié)點不包含完整的數(shù)據(jù)行,而是包含指向數(shù)據(jù)行的指針或主鍵值。數(shù)據(jù)行本身存儲在聚簇索引中。
索引與數(shù)據(jù)關系:由于數(shù)據(jù)與索引緊密相連,當通過聚簇索引查找數(shù)據(jù)時,可以直接從索引中獲得數(shù)據(jù)行,而不需要額外的步驟去查找數(shù)據(jù)所在的位置。當通過非聚簇索引查找數(shù)據(jù)時,首先在非聚簇索引中找到對應的主鍵值,然后通過這個主鍵值回溯到聚簇索引中查找實際的數(shù)據(jù)行,這個過程稱為“回表”。
唯一性:聚簇索引通常是基于主鍵構建的,因此每個表只能有一個聚簇索引,因為數(shù)據(jù)只能有一種物理排序方式。一個表可以有多個非聚簇索引,因為它們不直接影響數(shù)據(jù)的物理存儲位置。
效率:對于范圍查詢和排序查詢,聚簇索引通常更有效率,因為它避免了額外的尋址開銷。非聚簇索引在使用覆蓋索引進行查詢時效率更高,因為它不需要讀取完整的數(shù)據(jù)行。但是需要進行回表的操作,使用非聚簇索引效率比較低,因為需要進行額外的回表操作。
為什么MySQL索引使用B+樹?
B 樹和 B+ 都是通過多叉樹的方式,會將樹的高度變矮,所以這兩個數(shù)據(jù)結構非常適合檢索存于磁盤中的數(shù)據(jù)。但是 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+ 樹。
Redis
Redis有哪些數(shù)據(jù)結構
Redis 提供了豐富的數(shù)據(jù)類型,常見的有五種數(shù)據(jù)類型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
隨著 Redis 版本的更新,后面又支持了四種數(shù)據(jù)類型:BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。Redis 五種數(shù)據(jù)類型的應用場景:
- String 類型的應用場景:緩存對象、常規(guī)計數(shù)、分布式鎖、共享 session 信息等。List 類型的應用場景:消息隊列(但是有兩個問題:1. 生產(chǎn)者需要自行實現(xiàn)全局唯一 ID;2. 不能以消費組形式消費數(shù)據(jù))等。Hash 類型:緩存對象、購物車等。Set 類型:聚合計算(并集、交集、差集)場景,比如點贊、共同關注、抽獎活動等。Zset 類型:排序場景,比如排行榜、電話和姓名排序等。
Redis 后續(xù)版本又支持四種數(shù)據(jù)類型,它們的應用場景如下:
- BitMap(2.2 版新增):二值狀態(tài)統(tǒng)計的場景,比如簽到、判斷用戶登陸狀態(tài)、連續(xù)簽到用戶總數(shù)等;HyperLogLog(2.8 版新增):海量數(shù)據(jù)基數(shù)統(tǒng)計的場景,比如百萬級網(wǎng)頁 UV 計數(shù)等;GEO(3.2 版新增):存儲地理位置信息的場景,比如滴滴叫車;Stream(5.0 版新增):消息隊列,相比于基于 List 類型實現(xiàn)的消息隊列,有這兩個特有的特性:自動生成全局唯一消息ID,支持以消費組形式消費數(shù)據(jù)。
使用redis實現(xiàn)布隆過濾器?講一下布隆過濾器的原理?
布隆過濾器由「初始值都為 0 的位圖數(shù)組」和「 N 個哈希函數(shù)」兩部分組成。當我們在寫入數(shù)據(jù)庫數(shù)據(jù)時,在布隆過濾器里做個標記,這樣下次查詢數(shù)據(jù)是否在數(shù)據(jù)庫時,只需要查詢布隆過濾器,如果查詢到數(shù)據(jù)沒有被標記,說明不在數(shù)據(jù)庫中。
布隆過濾器會通過 3 個操作完成標記:
- 第一步,使用 N 個哈希函數(shù)分別對數(shù)據(jù)做哈希計算,得到 N 個哈希值;第二步,將第一步得到的 N 個哈希值對位圖數(shù)組的長度取模,得到每個哈希值在位圖數(shù)組的對應位置。第三步,將每個哈希值在位圖數(shù)組的對應位置的值設置為 1;
舉個例子,假設有一個位圖數(shù)組長度為 8,哈希函數(shù) 3 個的布隆過濾器。
在數(shù)據(jù)庫寫入數(shù)據(jù) x 后,把數(shù)據(jù) x 標記在布隆過濾器時,數(shù)據(jù) x 會被 3 個哈希函數(shù)分別計算出 3 個哈希值,然后在對這 3 個哈希值對 8 取模,假設取模的結果為 1、4、6,然后把位圖數(shù)組的第 1、4、6 位置的值設置為 1。當應用要查詢數(shù)據(jù) x 是否數(shù)據(jù)庫時,通過布隆過濾器只要查到位圖數(shù)組的第 1、4、6 位置的值是否全為 1,只要有一個為 0,就認為數(shù)據(jù) x 不在數(shù)據(jù)庫中。
布隆過濾器由于是基于哈希函數(shù)實現(xiàn)查找的,高效查找的同時存在哈希沖突的可能性,比如數(shù)據(jù) x 和數(shù)據(jù) y 可能都落在第 1、4、6 位置,而事實上,可能數(shù)據(jù)庫中并不存在數(shù)據(jù) y,存在誤判的情況。
所以,查詢布隆過濾器說數(shù)據(jù)存在,并不一定證明數(shù)據(jù)庫中存在這個數(shù)據(jù),但是查詢到數(shù)據(jù)不存在,數(shù)據(jù)庫中一定就不存在這個數(shù)據(jù)。
Redis的高可用體現(xiàn)在哪里?
Redis 主從架構
Redis 多副本,采用主從(replication)部署結構,相較于單副本而言最大的特點就是主從實例間數(shù)據(jù)實時同步,并且提供數(shù)據(jù)持久化和備份策略。主從實例部署在不同的物理服務器上,根據(jù)公司的基礎環(huán)境配置,可以實現(xiàn)同時對外提供服務和讀寫分離策略。
優(yōu)點:讀寫分離策略:從節(jié)點可以擴展主庫節(jié)點的讀能力,有效應對大并發(fā)量的讀操作。
缺點:故障恢復復雜,如果沒有 RedisHA 系統(tǒng)(需要開發(fā)),當主庫節(jié)點出現(xiàn)故障時,需要手動將一個從節(jié)點晉升為主節(jié)點,同時需要通知業(yè)務方變更配置,并且需要讓其它從庫節(jié)點去復制新主庫節(jié)點,整個過程需要人為干預,比較繁瑣;
Redis 哨兵機制
Redis Sentinel 集群是由若干 Sentinel 節(jié)點組成的分布式集群,可以實現(xiàn)故障發(fā)現(xiàn)、故障自動轉(zhuǎn)移、配置中心和客戶端通知。Redis Sentinel 的節(jié)點數(shù)量要滿足 2n+1(n>=1)的奇數(shù)個。
優(yōu)點:
- Redis Sentinel 集群,能夠解決 Redis 主從模式下的高可用切換問題;
Redis Cluster
Redis Cluster 是社區(qū)版推出的 Redis 分布式集群解決方案,主要解決 Redis 分布式方面的需求,比如,當遇到單機內(nèi)存,并發(fā)和流量等瓶頸的時候,Redis Cluster 能起到很好的負載均衡的目的。Redis Cluster 集群節(jié)點最小配置 6 個節(jié)點以上(3 主 3 從),其中主節(jié)點提供讀寫操作,從節(jié)點作為備用節(jié)點,不提供請求,只作為故障轉(zhuǎn)移使用。Redis Cluster 采用虛擬槽分區(qū),所有的鍵根據(jù)哈希函數(shù)映射到 0~16383 個整數(shù)槽內(nèi),每個節(jié)點負責維護一部分槽以及槽所印映射的鍵值數(shù)據(jù)。
優(yōu)點:
- 無中心架構,數(shù)據(jù)按照 slot 存儲分布在多個節(jié)點,節(jié)點間數(shù)據(jù)共享,可動態(tài)調(diào)整數(shù)據(jù)分布;可擴展性:可線性擴展到 1000 多個節(jié)點,節(jié)點可動態(tài)添加或刪除;高可用性:部分節(jié)點不可用時,集群仍可用。通過增加 Slave 做 standby 數(shù)據(jù)副本,能夠?qū)崿F(xiàn)故障自動 failover,節(jié)點之間通過 gossip 協(xié)議交換狀態(tài)信息,用投票機制完成 Slave 到 Master 的角色提升;
Redis集群分區(qū)為什么使用散列插槽而不是用一致性哈希?
- 當發(fā)生擴容時候,Redis可配置映射表的方式讓哈希槽更靈活,可更方便組織映射到新增server上面的slot數(shù),比一致性hash的算法更靈活方便。在數(shù)據(jù)遷移時,一致性hash 需要重新計算key在新增節(jié)點的數(shù)據(jù),然后遷移這部分數(shù)據(jù),哈希槽則直接將一個slot對應的數(shù)據(jù)全部遷移,實現(xiàn)更簡單??梢造`活的分配槽位,比如性能更好的節(jié)點分配更多槽位,性能相對較差的節(jié)點可以分配較少的槽位。
場景
redis如何實現(xiàn)防止超賣,加鎖加的是什么鎖?
使用redis 實現(xiàn)分布式鎖,同一個鎖key,同一時間只能有一個客戶端拿到鎖,其他客戶端會陷入無限的等待來嘗試獲取那個鎖,只有獲取到鎖的客戶端才能執(zhí)行下面的業(yè)務邏輯。
這種方案的缺點是同一個商品在多用戶同時下單的情況下,會基于分布式鎖串行化處理,導致沒法同時處理同一個商品的大量下單的請求。
如果不使用redis鎖,在并發(fā)的情況下,單獨依靠mysql怎么保證線程安全,防止超賣?
可以使用樂觀鎖的方案,更新數(shù)據(jù)庫減庫存的時候,進行庫存限制條件
update?goods?set?stock?=?stock?-?1?where goods_id =??and stock >0
消息隊列
Kafka怎么保證數(shù)據(jù)不丟失?
使用一個消息隊列,其實就分為三大塊:生產(chǎn)者、中間件、消費者,所以要保證消息就是保證三個環(huán)節(jié)都不能丟失數(shù)據(jù)。
消息生產(chǎn)階段:生產(chǎn)者會不會丟消息,取決于生產(chǎn)者對于異常情況的處理是否合理。從消息被生產(chǎn)出來,然后提交給 MQ 的過程中,只要能正常收到 ( MQ 中間件) 的 ack 確認響應,就表示發(fā)送成功,所以只要處理好返回值和異常,如果返回異常則進行消息重發(fā),那么這個階段是不會出現(xiàn)消息丟失的。
消息存儲階段:Kafka 在使用時是部署一個集群,生產(chǎn)者在發(fā)布消息時,隊列中間件通常會寫「多個節(jié)點」,也就是有多個副本,這樣一來,即便其中一個節(jié)點掛了,也能保證集群的數(shù)據(jù)不丟失。
消息消費階段:消費者接收消息+消息處理之后,才回復 ack 的話,那么消息階段的消息不會丟失。不能收到消息就回 ack,否則可能消息處理中途掛掉了,消息就丟失了。
Kafka為什么一個分區(qū)只能由消費者組的一個消費者消費?這樣設計的意義是什么?
同一時刻,一條消息只能被組中的一個消費者實例消費
如果兩個消費者負責同一個分區(qū),那么就意味著兩個消費者同時讀取分區(qū)的消息,由于消費者自己可以控制讀取消息的offset,就有可能C1才讀到2,而C1讀到1,C1還沒處理完,C2已經(jīng)讀到3了,則會造成很多浪費,因為這就相當于多線程讀取同一個消息,會造成消息處理的重復,且不能保證消息的順序。