Java并發(fā)編程面試題(2)
一、AQS高頻問題:
1.1 AQS是什么?
AQS就是一個抽象隊列同步器,abstract queued sychronizer,本質(zhì)就是一個抽象類。
AQS中有一個核心屬性state,其次還有一個雙向鏈表以及一個單項鏈表。
首先state是基于volatile修飾,再基于CAS修改,同時可以保證三大特性。(原子,可見,有序)
其次還提供了一個雙向鏈表。有Node對象組成的雙向鏈表。
最后在Condition內(nèi)部類中,還提供了一個由Node對象組成的單向鏈表。
AQS是JUC下大量工具的基礎(chǔ)類,很多工具都基于AQS實現(xiàn)的,比如lock鎖,CountDownLatch,Semaphore,線程池等等都用到了AQS。
state是啥:state就是一個int類型的數(shù)值,同步狀態(tài),至于到底是什么狀態(tài),看子類實現(xiàn)。
condition和單向鏈表是啥:都知道sync內(nèi)部提供了wait方法和notify方法的使用,lock鎖也需要實現(xiàn)這種機制,lock鎖就基于AQS內(nèi)部的Condition實現(xiàn)了await和signal方法。(對標sync的wait和notify)
sync在線程持有鎖時,執(zhí)行wait方法,會將線程扔到WaitSet等待池中排隊,等待喚醒
lcok在線程持有鎖時,執(zhí)行await方法,會將線程封裝為Node對象,扔到Condition單向鏈表中,等待喚醒
Condition在做了什么:將持有鎖的線程封裝為Node扔到Condition單向鏈表,同時掛起線程。如果線程喚醒了,就將Condition中的Node扔到AQS的雙向鏈表等待獲取鎖。
1.2 喚醒線程時,AQS為什么從后往前遍歷?
如果線程沒有獲取到資源,就需要將線程封裝為Node對象,安排到AQS的雙向鏈表中排隊,并且可能會掛起線程
如果在喚醒線程時,head節(jié)點的next是第一個要被喚醒的,如果head的next節(jié)點取消了,AQS的邏輯是從tail節(jié)點往前遍歷,找到離head最近的有效節(jié)點?
想解釋清楚這個問題,需要先了解,一個Node對象,是如何添加到雙向鏈表中的。
基于addWaiter方法中,是先將當前Node的prev指向tail的節(jié)點,再將tail指向我自己,再讓prev節(jié)點指向我
如下圖,如果只執(zhí)行到了2步驟,此時,Node加入到了AQS隊列中,但是從prev節(jié)點往后,會找不到當前節(jié)點。

1.3 AQS為什么用雙向鏈表,(為啥不用單向鏈表)?
因為AQS中,存在取消節(jié)點的操作,節(jié)點被取消后,需要從AQS的雙向鏈表中斷開連接。
還需要保證雙向鏈表的完整性,
需要將prev節(jié)點的next指針,指向next節(jié)點。
需要將next節(jié)點的prev指針,指向prev節(jié)點。
如果正常的雙向鏈表,直接操作就可以了。
但是如果是單向鏈表,需要遍歷整個單向鏈表才能完成的上述的操作。比較浪費資源。
1.4 AQS為什么要有一個虛擬的head節(jié)點
有一個哨兵節(jié),點更方便操作。
另一個是因為AQS內(nèi)部,每個Node都會有一些狀態(tài),這個狀態(tài)不單單針對自己,還針對后續(xù)節(jié)點
1:當前節(jié)點取消了。
0:默認狀態(tài),啥事沒有。
-1:當前節(jié)點的后繼節(jié)點,掛起了。
-2:代表當前節(jié)點在Condition隊列中(await將線程掛起了)
-3:代表當前是共享鎖,喚醒時,后續(xù)節(jié)點依然需要被喚醒。
Node節(jié)點的ws,表示很多信息,除了當前節(jié)點的狀態(tài),還會維護后繼節(jié)點狀態(tài)。
如果取消虛擬的head節(jié)點,一個節(jié)點無法同時保存當前階段狀態(tài)和后繼節(jié)點狀態(tài)。
同時,在釋放鎖資源時,就要基于head節(jié)點的狀態(tài)是否是-1。來決定是否喚醒后繼節(jié)點。
如果為-1,正常喚醒
如果不為-1,不需要喚醒嗎,減少了一次可能發(fā)生的遍歷操作,提升性能。
1.5 ReentrantLock的底層實現(xiàn)原理
ReentrantLock是基于AQS實現(xiàn)的。
在線程基于ReentrantLock加鎖時,需要基于CAS去修改state屬性,如果能從0改為1,代表獲取鎖資源成功
如果CAS失敗了,添加到AQS的雙向鏈表中排隊(可能會掛起線程),等待獲取鎖。
持有鎖的線程,如果執(zhí)行了condition的await方法,線程會封裝為Node添加到Condition的單向鏈表中,等待被喚醒并且重新競爭鎖資源
Java中除了一會講到的線程池中Worker的鎖之外,都是可重入鎖。
在基礎(chǔ)面試題2中,波波老師說到了:Synchronizer和ReentrantLock的區(qū)別?
1.6 ReentrantLock的公平鎖和非公平鎖的區(qū)別
注意,想回答的準確些,就別舉生活中的列子,用源碼的角度去說。
如果用生活中的例子,你就就要用有些人沒素質(zhì),但是又有素質(zhì)。
源碼:
公平鎖和非公平中的lock方法和tryAcquire方法的實現(xiàn)有一內(nèi)內(nèi)不同,其他都一樣
如果沒有排隊的,直接嘗試將state從 0 ?~ 1
如果有排隊的,第一名不是我,不搶,繼續(xù)等待。
如果有排隊的,我是第一名,直接嘗試將state從 0 ?~ 1
非公平鎖lock:直接嘗試將state從 0 ?~ 1,如果成功,拿鎖直接走,如果失敗了,執(zhí)行tryAcquire
公平鎖lock:直接執(zhí)行tryAcquire
非公平鎖tryAcquire:如果當前沒有線程持有鎖資源,直接再次嘗試將state從 0 ?~ 1如果成功,拿鎖直接走
公平鎖tryAcquire:如果當前沒有線程持有鎖資源,先看一下,有排隊的么。
如果都沒拿到鎖,公平鎖和非公平鎖的后續(xù)邏輯是一樣的,排隊后,就不存在所謂的插隊。
生活的例子:非公平鎖會有機會嘗試強行獲取鎖資源兩次,成功開開心心走人,失敗,消消停停去排隊。
有個人前來做核酸
有人正在扣嗓子眼么?
沒人正在被扣,上去嘗試做凳子上!成功了,扣完走人。
如果有人正在扣,消停去排隊。
公平鎖:先看眼,有排隊的么,有就去排隊
非公平鎖:不管什么情況,先嘗試做凳子上。如果坐上了,直接被扣,扣完走人,如果沒做到凳子上
1.7 ReentrantReadWriteLock如何實現(xiàn)的讀寫鎖
如果一個操作寫少讀多,還用互斥鎖的話,性能太低,因為讀讀不存在并發(fā)問題。
怎么解決啊,有讀寫鎖的出現(xiàn)。
ReentrantReadWriteLock也是基于AQS實現(xiàn)的一個讀寫鎖,但是鎖資源用state標識。
如何基于一個int來標識兩個鎖信息,有寫鎖,有讀鎖,怎么做的?
一個int,占了32個bit位。
在寫鎖獲取鎖時,基于CAS修改state的低16位的值。
在讀鎖獲取鎖時,基于CAS修改state的高16位的值。
寫鎖的重入,基于state低16直接標識,因為寫鎖是互斥的。
讀鎖的重入,無法基于state的高16位去標識,因為讀鎖是共享的,可以多個線程同時持有。所以讀鎖的重入用的是ThreadLocal來表示,同時也會對state的高16為進行追加。
二、阻塞隊列高頻問題:
2.1 說下你熟悉的阻塞隊列?
ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue
ArrayBlockingQueue:底層基于數(shù)組實現(xiàn),記得new的時候設(shè)置好邊界。
LinkedBlockingQueue:底層基于鏈表實現(xiàn)的,可以認為是無界隊列,但是可以設(shè)置長度。
PriorityBlockingQueue:底層是基于數(shù)組實現(xiàn)的二叉堆,可以認為是無界隊列,因為數(shù)組會擴容。
ArrayBlockingQueue,LinkedBlockingQueue是ThreadPoolExecutor線程池最常用的兩個阻塞隊列。
PriorityBlockingQueue:是ScheduleThreadPoolExecutor定時任務線程池用的阻塞隊列跟PriorityBlockingQueue的底層實現(xiàn)是一樣的。(其實本質(zhì)用的是DelayWorkQueue)
2.2 虛假喚醒是什么?
虛假喚醒在阻塞隊列的源碼中就有體現(xiàn)。
比如消費者1在消費數(shù)據(jù)時,會先判斷隊列是否有元素,如果元素個數(shù)為0,消費者1會掛起。
此處判斷元素為0的位置,如果用if循環(huán)會導致出現(xiàn)一個問題。
如果生產(chǎn)者添加了一個數(shù)據(jù),會喚醒消費者1。
但是如果消費者1沒拿到鎖資源,消費者2拿到了鎖資源并帶走了數(shù)據(jù)的話。
消費者1再次拿到鎖資源時,無法從隊列獲取到任何元素。導致出現(xiàn)邏輯問題。
解決方案,將判斷元素個數(shù)的位置,設(shè)置為while判斷。
三、線程池高頻問題:(最重要的點)
3.1 線程池的7個參數(shù)(不會就回家等通知)
核心線程數(shù),最大線程數(shù),最大空閑時間,時間單位,阻塞隊列,線程工廠,拒絕策略
3.2 線程池的狀態(tài)有什么,如何記錄的?
線程池不是什么時候都接活的!
線程池有5個狀態(tài)。、

線程池的狀態(tài)是在ctl屬性中記錄的。本質(zhì)就是int類型

ctl的高三位記錄線程池狀態(tài)
低29位,記錄工作線程個數(shù)。即便你指定的線程最大數(shù)量是Integer.MAX_VALUE他也到不了
3.3 線程池常見的拒絕策略(不會就回家等通知)
AbortPolicy:拋異常(默認)

CallerRunsPolicy,誰提交的任務,誰執(zhí)行。異步變同步

DiscardPolicy:任務直接不要

DiscardOldestPolicy:把最早放過來的任務丟失,再次嘗試將當前任務交給線程池處理

一般情況下,線程池自帶的無法滿足業(yè)務時,自定義一個線程池的拒絕策略。
實現(xiàn)下面的接口即可。

3.4 線程池執(zhí)行流程(不會就回家等通知)
核心線程不是new完就構(gòu)建的,是懶加載的機制,添加任務才會構(gòu)建核心線程
2個核心線程 5個最大線程 ?阻塞隊列長度為2

3.5 線程池為什么添加空任務的非核心線程

避免線程池出現(xiàn)工作隊列有任務,但是沒有工作線程處理。
線程池可以設(shè)置核心線程數(shù)是0個。這樣,任務扔到阻塞隊列,但是沒有工作線程,這不涼涼了么~~
線程池中的核心線程不是一定不會被回收,線程池中有一個屬性,如果設(shè)置為true,核心線程也會被干掉

3.6 在沒任務時,線程池中的工作線程在干嘛?
線程會掛起,默認核心線程是WAITING狀態(tài),非核心是TIMED_WAITING
如果是核心線程,默認情況下,會在阻塞隊列的位置執(zhí)行take方法,直到拿到任務為止。
如果是非核心線程,默認情況下,會在阻塞隊列的位置執(zhí)行poll方法,等待最大空閑時間,如果沒任務,直接拉走咔嚓掉,如果有活,那就正常干。
3.7 工作線程出現(xiàn)異常會導致什么問題?
是否拋出異常、影響其他線程嗎、工作線程會嘎嘛?
如果任務是execute方法執(zhí)行的,工作線程會將異常拋出。
如果任務是submit方法執(zhí)行的futureTask,工作線程會將異常捕獲并保存到FutureTask里,可以基于futureTask的get得到異常信息
出現(xiàn)異常的工作線程不會影響到其他的工作線程。
runWorker中的異常會被拋到run方法中,run方法會異常結(jié)束,run方法結(jié)束,線程就嘎了!
如果是submit,異常沒拋出來,那就不嘎~
3.8 工作線程繼承AQS的目的是什么?
工作線程的本質(zhì),就是Worker對象
繼承AQS跟shutdown和shutdownNow有關(guān)系。
如果是shutdown,會中斷空閑的工作線程,基于Worker實現(xiàn)的AQS中的state的值來判斷能否中斷工作線程。
如果工作線程的state是0,代表空閑,可以中斷,如果是1,代表正在干活。
如果是shutdownNow,直接強制中斷所有工作線程
3.9 核心參數(shù)怎么設(shè)置?
如果面試問到,你項目中的線程池參數(shù)設(shè)置的是多少,你先給個準確的數(shù)字和配置。別上來就說怎么設(shè)置??!
線程池的目的是為了充分發(fā)揮CPU的資源。提升整個系統(tǒng)的性能。
系統(tǒng)內(nèi)部不同業(yè)務的線程池參考的方式也不一樣。
如果是CPU密集的任務,一般也就是CPU內(nèi)核數(shù) + 1的核心線程數(shù)。這樣足以充分發(fā)揮CPU性能。
如果是IO密集的任務,因為IO的程度不一樣的啊,有的是1s,有的是1ms,有的是1分鐘,所以IO密集的任務在用線程池處理時,一定要通過壓測的方式,觀察CPU資源的占用情況,來決定核心線程數(shù)。一般發(fā)揮CPU性能到70~80足矣。所以線程池的參數(shù)設(shè)置需要通過壓測以及多次調(diào)整才能得出具體的。
比如一個業(yè)務要查詢?nèi)齻€服務