2021最新一线互联网大厂常见高并发面试题解析
(一)高并發(fā)編程基礎(chǔ)知識
這里涉及到一些基礎(chǔ)的概念,我重新捧起了一下《實戰(zhàn) Java 高并發(fā)程序設(shè)計》這一本書,感覺到心潮澎湃,這或許就是筆者敘述功底扎實的魅力吧,喜歡。
作為閱讀福利,我把大廠常見高并發(fā)面試題以及知識點整理成了pdf文檔,現(xiàn)在免費分享給閱讀到本篇文章的Java程序員朋友們,需要的點擊下方鏈接領(lǐng)取!!
最全學(xué)習(xí)筆記大廠真題+微服務(wù)+MySQL+分布式+SSM框架+Java+Redis+數(shù)據(jù)結(jié)構(gòu)與算法+網(wǎng)絡(luò)+Linux+Spring全家桶+JVM+高并發(fā)+各大學(xué)習(xí)思維腦圖+面試集合
1)多線程和單線程的區(qū)別和聯(lián)系?
答:
在單核 CPU 中,將 CPU 分為很小的時間片,在每一時刻只能有一個線程在執(zhí)行,是一種微觀上輪流占用 CPU 的機制。
多線程會存在線程上下文切換,會導(dǎo)致程序執(zhí)行速度變慢,即采用一個擁有兩個線程的進(jìn)程執(zhí)行所需要的時間比一個線程的進(jìn)程執(zhí)行兩次所需要的時間要多一些。
結(jié)論:即采用多線程不會提高程序的執(zhí)行速度,反而會降低速度,但是對于用戶來說,可以減少用戶的響應(yīng)時間。
面試官:那使用多線程有什么優(yōu)勢?
解析:盡管面臨很多挑戰(zhàn),多線程有一些優(yōu)點仍然使得它一直被使用,而這些優(yōu)點我們應(yīng)該了解。
答:
(1)資源利用率更好
想象一下,一個應(yīng)用程序需要從本地文件系統(tǒng)中讀取和處理文件的情景。比方說,從磁盤讀取一個文件需要5秒,處理一個文件需要2秒。處理兩個文件則需要:
1| 5秒讀取文件A 2| 2秒處理文件A 3| 5秒讀取文件B 4| 2秒處理文件B 5| --------------------- 6| 總共需要14秒從磁盤中讀取文件的時候,大部分的CPU時間用于等待磁盤去讀取數(shù)據(jù)。在這段時間里,CPU非常的空閑。它可以做一些別的事情。通過改變操作的順序,就能夠更好的使用CPU資源。看下面的順序:
1| 5秒讀取文件A 2| 5秒讀取文件B + 2秒處理文件A 3| 2秒處理文件B 4| --------------------- 5| 總共需要12秒CPU等待第一個文件被讀取完。然后開始讀取第二個文件。當(dāng)?shù)诙募诒蛔x取的時候,CPU會去處理第一個文件。記住,在等待磁盤讀取文件的時候,CPU大部分時間是空閑的。
總的說來,CPU能夠在等待IO的時候做一些其他的事情。這個不一定就是磁盤IO。它也可以是網(wǎng)絡(luò)的IO,或者用戶輸入。通常情況下,網(wǎng)絡(luò)和磁盤的IO比CPU和內(nèi)存的IO慢的多。
(2)程序設(shè)計在某些情況下更簡單
在單線程應(yīng)用程序中,如果你想編寫程序手動處理上面所提到的讀取和處理的順序,你必須記錄每個文件讀取和處理的狀態(tài)。相反,你可以啟動兩個線程,每個線程處理一個文件的讀取和操作。線程會在等待磁盤讀取文件的過程中被阻塞。在等待的時候,其他的線程能夠使用CPU去處理已經(jīng)讀取完的文件。其結(jié)果就是,磁盤總是在繁忙地讀取不同的文件到內(nèi)存中。這會帶來磁盤和CPU利用率的提升。而且每個線程只需要記錄一個文件,因此這種方式也很容易編程實現(xiàn)。
(3)程序響應(yīng)更快
有時我們會編寫一些較為復(fù)雜的代碼(這里的復(fù)雜不是說復(fù)雜的算法,而是復(fù)雜的業(yè)務(wù)邏輯),例如,一筆訂單的創(chuàng)建,它包括插入訂單數(shù)據(jù)、生成訂單趕快找、發(fā)送郵件通知賣家和記錄貨品銷售數(shù)量等。用戶從單擊“訂購”按鈕開始,就要等待這些操作全部完成才能看到訂購成功的結(jié)果。但是這么多業(yè)務(wù)操作,如何能夠讓其更快地完成呢?
在上面的場景中,可以使用多線程技術(shù),即將數(shù)據(jù)一致性不強的操作派發(fā)給其他線程處理(也可以使用消息隊列),如生成訂單快照、發(fā)送郵件等。這樣做的好處是響應(yīng)用戶請求的線程能夠盡可能快地處理完成,縮短了響應(yīng)時間,提升了用戶體驗。
多線程還有一些優(yōu)勢也顯而易見:
① 進(jìn)程之前不能共享內(nèi)存,而線程之間共享內(nèi)存(堆內(nèi)存)則很簡單。
② 系統(tǒng)創(chuàng)建進(jìn)程時需要為該進(jìn)程重新分配系統(tǒng)資源,創(chuàng)建線程則代價小很多,因此實現(xiàn)多任務(wù)并發(fā)時,多線程效率更高.
③ Java語言本身內(nèi)置多線程功能的支持,而不是單純第作為底層系統(tǒng)的調(diào)度方式,從而簡化了多線程編程.
2)多線程一定快嗎?
答:不一定。
比如,我們嘗試使用并行和串行來分別執(zhí)行累加的操作觀察是否并行執(zhí)行一定比串行執(zhí)行更快:
以下是我測試的結(jié)果,可以看出,當(dāng)不超過1百萬的時候,并行是明顯比串行要慢的,為什么并發(fā)執(zhí)行的速度會比串行慢呢?這是因為線程有創(chuàng)建和上下文切換的開銷。
3)什么是同步?什么又是異步?
解析:這是對多線程基礎(chǔ)知識的考察
答:同步和異步通常用來形容一次方法調(diào)用。
同步方法調(diào)用一旦開始,調(diào)用者必須等到方法返回后,才能繼續(xù)后續(xù)的行為。這就好像是我們?nèi)ド坛琴I一臺空調(diào),你看中了一臺空調(diào),于是就跟售貨員下了單,然后售貨員就去倉庫幫你調(diào)配物品,這天你熱的實在不行,就催著商家趕緊發(fā)貨,于是你就在商店里等著,知道商家把你和空調(diào)都送回家,一次愉快的購物才結(jié)束,這就是同步調(diào)用。
而異步方法更像是一個消息傳遞,一旦開始,方法調(diào)用就會立即返回,調(diào)用者就可以繼續(xù)后續(xù)的操作。回到剛才買空調(diào)的例子,我們可以坐在里打開電腦,在網(wǎng)上訂購一臺空調(diào)。當(dāng)你完成網(wǎng)上支付的時候,對你來說購物過程已經(jīng)結(jié)束了。雖然空調(diào)還沒有送到家,但是你的任務(wù)都已經(jīng)完成了。商家接到你的訂單后,就會加緊安排送貨,當(dāng)然這一切已經(jīng)跟你無關(guān)了,你已經(jīng)支付完成,想什么就能去干什么了,出去溜達(dá)幾圈都不成問題。等送貨上門的時候,接到商家電話,回家一趟簽收即可。這就是異步調(diào)用。
面試官:那并發(fā)(Concurrency)和并行(Parallelism)的區(qū)別呢?
解析:并行性和并發(fā)性是既相似又有區(qū)別的兩個概念。
答:并行性是指兩個或多個事件在同一時刻發(fā)生。而并發(fā)性是指連個或多個事件在同一時間間隔內(nèi)發(fā)生。
在多道程序環(huán)境下,并發(fā)性是指在一段時間內(nèi)宏觀上有多個程序在同時運行,但在單處理機環(huán)境下(一個處理器),每一時刻卻僅能有一道程序執(zhí)行,故微觀上這些程序只能是分時地交替執(zhí)行。例如,在1秒鐘時間內(nèi),0-15ms程序A運行;15-30ms程序B運行;30-45ms程序C運行;45-60ms程序D運行,因此可以說,在1秒鐘時間間隔內(nèi),宏觀上有四道程序在同時運行,但微觀上,程序A、B、C、D是分時地交替執(zhí)行的。
如果在計算機系統(tǒng)中有多個處理機,這些可以并發(fā)執(zhí)行的程序就可以被分配到多個處理機上,實現(xiàn)并發(fā)執(zhí)行,即利用每個處理機處理一個可并發(fā)執(zhí)行的程序。這樣,多個程序便可以同時執(zhí)行。以此就能提高系統(tǒng)中的資源利用率,增加系統(tǒng)的吞吐量。
4)線程和進(jìn)程的區(qū)別:(必考)
答:
-
(1)進(jìn)程切換時,涉及到當(dāng)前進(jìn)程的 CPU 環(huán)境的保存和新被調(diào)度運行進(jìn)程的 CPU 環(huán)境的設(shè)置。
-
(2)線程切換僅需要保存和設(shè)置少量的寄存器內(nèi)容,不涉及存儲管理方面的操作。
面試官:進(jìn)程間如何通訊?線程間如何通訊?
答:進(jìn)程間通訊依靠 IPC 資源,例如管道(pipes)、套接字(sockets)等;
線程間通訊依靠 JVM 提供的 API,例如 wait()、notify()、notifyAll() 等方法,線程間還可以通過共享的主內(nèi)存來進(jìn)行值的傳遞。
5)什么是阻塞(Blocking)和非阻塞(Non-Blocking)?
答:阻塞和非阻塞通常用來形容多線程間的相互影響。比如一個線程占用了臨界區(qū)資源,那么其他所有需要這個而資源的線程就必須在這個臨界區(qū)中進(jìn)行等待。等待會導(dǎo)致線程掛起,這種情況就是阻塞。此時,如果占用資源的線程一直不愿意釋放資源,那么其他所有阻塞在這個臨界區(qū)上的線程都不能工作。
非阻塞的意思與之相反,它強調(diào)沒有一個線程可以妨礙其他線程執(zhí)行。所有的線程都會嘗試不斷前向執(zhí)行。
面試官:臨界區(qū)是什么?
答:臨界區(qū)用來表示一種公共資源或者說是共享資源,可以被多個線程使用。但是每一次,只能有一個線程使用它,一旦臨界區(qū)資源被占用,其他線程要想使用這個資源,就必須等待。
比如,在一個辦公室里有一臺打印機,打印機一次只能執(zhí)行一個任務(wù)。如果小王和小明同時需要打印文件,很顯然,如果小王先下發(fā)了打印任務(wù),打印機就開始打印小王的文件了,小明的任務(wù)就只能等待小王打印結(jié)束后才能打印,這里的打印機就是一個臨界區(qū)的例子。
在并行程序中,臨界區(qū)資源是保護的對象,如果意外出現(xiàn)打印機同時執(zhí)行兩個打印任務(wù),那么最可能的結(jié)果就是打印出來的文件就會是損壞的文件,它既不是小王想要的,也不是小明想要的。
6)什么是死鎖(Deadlock)、饑餓(Starvation)和活鎖(Livelock)?
答:死鎖、饑餓和活鎖都屬于多線程的活躍性問題,如果發(fā)現(xiàn)上述幾種情況,那么相關(guān)線程可能就不再活躍,也就說它可能很難再繼續(xù)往下執(zhí)行了。
死鎖應(yīng)該是最糟糕的一種情況了,它表示兩個或者兩個以上的進(jìn)程在執(zhí)行過程中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無外力作用,它們都將無法推進(jìn)下去。此時稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖,這些永遠(yuǎn)在互相等待的進(jìn)程稱為死鎖進(jìn)程。
饑餓是指某一個或者多個線程因為種種原因無法獲得所需要的資源,導(dǎo)致一直無法執(zhí)行。比如:
1)它的線程優(yōu)先級可能太低,而高優(yōu)先級的線程不斷搶占它需要的資源,導(dǎo)致低優(yōu)先級的線程無法工作。在自然界中,母雞喂食雛鳥時,很容易出現(xiàn)這種情況,由于雛鳥很多,食物有限,雛鳥之間的食物競爭可能非常厲害,小雛鳥因為經(jīng)常搶不到食物,有可能會被餓死。線程的饑餓也非常類似這種情況。
2)另外一種可能是,某一個線程一直占著關(guān)鍵資源不放,導(dǎo)致其他需要這個資源的線程無法正常執(zhí)行,這種情況也是饑餓的一種。
與死鎖相比,饑餓還是有可能在未來一段時間內(nèi)解決的(比如高優(yōu)先級的線程已經(jīng)完成任務(wù),不再瘋狂的執(zhí)行)
活鎖是一種非常有趣的情況。不知道大家是不是有遇到過這樣一種情況,當(dāng)你要坐電梯下樓,電梯到了,門開了,這時你正準(zhǔn)備出去,但不巧的是,門外一個人擋著你的去路,他想進(jìn)來。于是你很紳士的靠左走,避讓對方,但同時對方也很紳士,但他靠右走希望避讓你。結(jié)果,你們又撞上了。于是乎,你們都意識到了問題,希望盡快避讓對方,你立即向右走,他也立即向左走,結(jié)果又撞上了!不過介于人類的只能,我相信這個動作重復(fù) 2、 3 次后,你應(yīng)該可以順利解決這個問題,因為這個時候,大家都會本能的對視,進(jìn)行交流,保證這種情況不再發(fā)生。
但如果這種情況發(fā)生在兩個線程間可能就不會那么幸運了,如果線程的智力不夠,且都秉承著 “謙讓” 的原則,主動將資源釋放給他人使用,那么就會出現(xiàn)資源不斷在兩個線程中跳動,而沒有一個線程可以同時拿到所有的資源而正常執(zhí)行。這種情況就是活鎖。
7)多線程產(chǎn)生死鎖的 4 個必要條件?
答:
互斥條件:一個資源每次只能被一個線程使用;
請求與保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不放;
不剝奪條件:進(jìn)程已經(jīng)獲得的資源,在未使用完之前,不能強行剝奪;
循環(huán)等待條件:若干線程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。
面試官:如何避免死鎖?(經(jīng)常接著問這個問題哦~)
答:指定獲取鎖的順序,舉例如下:
比如某個線程只有獲得 A 鎖和 B 鎖才能對某資源進(jìn)行操作,在多線程條件下,如何避免死鎖?
獲得鎖的順序是一定的,比如規(guī)定,只有獲得 A 鎖的線程才有資格獲取 B 鎖,按順序獲取鎖就可以避免死鎖!!!
8)如何指定多個線程的執(zhí)行順序?
解析:面試官會給你舉個例子,如何讓 10 個線程按照順序打印 0123456789?(寫代碼實現(xiàn))
答:
設(shè)定一個 orderNum,每個線程執(zhí)行結(jié)束之后,更新 orderNum,指明下一個要執(zhí)行的線程。并且喚醒所有的等待線程。
在每一個線程的開始,要 while 判斷 orderNum 是否等于自己的要求值!!不是,則 wait,是則執(zhí)行本線程。
9)Java 中線程有幾種狀態(tài)?
答:六種(查看 Java 源碼也可以看到是 6 種),并且某個時刻 Java 線程只能處于其中的一個狀態(tài)。
新建(NEW)狀態(tài):表示新創(chuàng)建了一個線程對象,而此時線程并沒有開始執(zhí)行。
可運行(RUNNABLE)狀態(tài):線程對象創(chuàng)建后,其它線程(比如 main 線程)調(diào)用了該對象的 start() 方法,才表示線程開始執(zhí)行。當(dāng)線程執(zhí)行時,處于 RUNNBALE 狀態(tài),表示線程所需的一切資源都已經(jīng)準(zhǔn)備好了。該狀態(tài)的線程位于可運行線程池中,等待被線程調(diào)度選中,獲取 cpu 的使用權(quán)。
阻塞(BLOCKED)狀態(tài):如果線程在執(zhí)行過程終于到了 synchronized 同步塊,就會進(jìn)入 BLOCKED 阻塞狀態(tài),這時線程就會暫停執(zhí)行,直到獲得請求的鎖。
等待(WAITING)狀態(tài):當(dāng)線程等待另一個線程通知調(diào)度器一個條件時,它自己進(jìn)入等待狀態(tài)。在調(diào)用Object.wait方法或Thread.join方法,或者是等待java.util.concurrent庫中的Lock或Condition時,就會出現(xiàn)這種情況;
計時等待(TIMED_WAITING)狀態(tài):Object.wait、Thread.join、Lock.tryLock和Condition.await 等方法有超時參數(shù),還有 Thread.sleep 方法、LockSupport.parkNanos 方法和 LockSupport.parkUntil 方法,這些方法會導(dǎo)致線程進(jìn)入計時等待狀態(tài),如果超時或者出現(xiàn)通知,都會切換會可運行狀態(tài);
終止(TERMINATED)狀態(tài):當(dāng)線程執(zhí)行完畢,則進(jìn)入該狀態(tài),表示結(jié)束。
注意:從 NEW 狀態(tài)出發(fā)后,線程不能再回到 NEW 狀態(tài),同理,處于 TERMINATED 狀態(tài)的線程也不能再回到 RUNNABLE 狀態(tài)。
(二)高并發(fā)編程-JUC 包
在 Java 5.0 提供了 java.util.concurrent(簡稱 JUC )包,在此包中增加了在并發(fā)編程中很常用的實用工具類,用于定義類似于線程的自定義子系統(tǒng),包括線程池、異步 IO 和輕量級任務(wù)框架。
1)sleep( ) 和 wait( n)、wait( ) 的區(qū)別:
答:
sleep 方法:是 Thread 類的靜態(tài)方法,當(dāng)前線程將睡眠 n 毫秒,線程進(jìn)入阻塞狀態(tài)。當(dāng)睡眠時間到了,會解除阻塞,進(jìn)行可運行狀態(tài),等待 CPU 的到來。睡眠不釋放鎖(如果有的話);
wait 方法:是 Object 的方法,必須與 synchronized 關(guān)鍵字一起使用,線程進(jìn)入阻塞狀態(tài),當(dāng) notify 或者 notifyall 被調(diào)用后,會解除阻塞。但是,只有重新占用互斥鎖之后才會進(jìn)入可運行狀態(tài)。睡眠時,釋放互斥鎖。
2)synchronized 關(guān)鍵字:
答:底層實現(xiàn):
進(jìn)入時,執(zhí)行 monitorenter,將計數(shù)器 +1,釋放鎖 monitorexit 時,計數(shù)器-1;
當(dāng)一個線程判斷到計數(shù)器為 0 時,則當(dāng)前鎖空閑,可以占用;反之,當(dāng)前線程進(jìn)入等待狀態(tài)。
含義:(monitor 機制)
Synchronized 是在加鎖,加對象鎖。對象鎖是一種重量鎖(monitor),synchronized 的鎖機制會根據(jù)線程競爭情況在運行時會有偏向鎖(單一線程)、輕量鎖(多個線程訪問 synchronized 區(qū)域)、對象鎖(重量鎖,多個線程存在競爭的情況)、自旋鎖等。
該關(guān)鍵字是一個幾種鎖的封裝。
3)volatile 關(guān)鍵字:
答:該關(guān)鍵字可以保證可見性不保證原子性。
功能:
主內(nèi)存和工作內(nèi)存,直接與主內(nèi)存產(chǎn)生交互,進(jìn)行讀寫操作,保證可見性;
禁止 JVM 進(jìn)行的指令重排序。
解析:關(guān)于指令重排序的問題,可以查閱 DCL 雙檢鎖失效相關(guān)資料。
4)volatile 能使得一個非原子操作變成原子操作嗎?
答:能。
一個典型的例子是在類中有一個 long 類型的成員變量。如果你知道該成員變量會被多個線程訪問,如計數(shù)器、價格等,你最好是將其設(shè)置為 volatile。為什么?因為 Java 中讀取 long 類型變量不是原子的,需要分成兩步,如果一個線程正在修改該 long 變量的值,另一個線程可能只能看到該值的一半(前 32 位)。但是對一個 volatile 型的 long 或 double 變量的讀寫是原子。
面試官:volatile 修飾符的有過什么實踐?
答:
一種實踐是用 volatile 修飾 long 和 double 變量,使其能按原子類型來讀寫。double 和 long 都是64位寬,因此對這兩種類型的讀是分為兩部分的,第一次讀取第一個 32 位,然后再讀剩下的 32 位,這個過程不是原子的,但 Java 中 volatile 型的 long 或 double 變量的讀寫是原子的。
volatile 修復(fù)符的另一個作用是提供內(nèi)存屏障(memory barrier),例如在分布式框架中的應(yīng)用。簡單的說,就是當(dāng)你寫一個 volatile 變量之前,Java 內(nèi)存模型會插入一個寫屏障(write barrier),讀一個 volatile 變量之前,會插入一個讀屏障(read barrier)。意思就是說,在你寫一個 volatile 域時,能保證任何線程都能看到你寫的值,同時,在寫之前,也能保證任何數(shù)值的更新對所有線程是可見的,因為內(nèi)存屏障會將其他所有寫的值更新到緩存。
5)ThreadLocal(線程局部變量)關(guān)鍵字:
答:當(dāng)使用 ThreadLocal 維護變量時,其為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立的改變自己的副本,而不會影響其他線程對應(yīng)的副本。
ThreadLocal 內(nèi)部實現(xiàn)機制:
每個線程內(nèi)部都會維護一個類似 HashMap 的對象,稱為 ThreadLocalMap,里邊會包含若干了 Entry(K-V 鍵值對),相應(yīng)的線程被稱為這些 Entry 的屬主線程;
Entry 的 Key 是一個 ThreadLocal 實例,Value 是一個線程特有對象。Entry 的作用即是:為其屬主線程建立起一個 ThreadLocal 實例與一個線程特有對象之間的對應(yīng)關(guān)系;
Entry 對 Key 的引用是弱引用;Entry 對 Value 的引用是強引用。
6)線程池有了解嗎?(必考)
答:java.util.concurrent.ThreadPoolExecutor 類就是一個線程池。客戶端調(diào)用 ThreadPoolExecutor.submit(Runnable task) 提交任務(wù),線程池內(nèi)部維護的工作者線程的數(shù)量就是該線程池的線程池大小,有 3 種形態(tài):
- 當(dāng)前線程池大小 :表示線程池中實際工作者線程的數(shù)量;
- 最大線程池大小 (maxinumPoolSize):表示線程池中允許存在的工作者線程的數(shù)量上限;
- 核心線程大小 (corePoolSize ):表示一個不大于最大線程池大小的工作者線程數(shù)量上限。
如果運行的線程少于 corePoolSize,則 Executor 始終首選添加新的線程,而不進(jìn)行排隊;
如果運行的線程等于或者多于 corePoolSize,則 Executor 始終首選將請求加入隊列,而不是添加新線程;
如果無法將請求加入隊列,即隊列已經(jīng)滿了,則創(chuàng)建新的線程,除非創(chuàng)建此線程超出 maxinumPoolSize, 在這種情況下,任務(wù)將被拒絕。
面試官:我們?yōu)槭裁匆褂镁€程池?
答:
減少創(chuàng)建和銷毀線程的次數(shù),每個工作線程都可以被重復(fù)利用,可執(zhí)行多個任務(wù)。
可以根據(jù)系統(tǒng)的承受能力,調(diào)整線程池中工作線程的數(shù)目,放置因為消耗過多的內(nèi)存,而把服務(wù)器累趴下(每個線程大約需要 1 MB 內(nèi)存,線程開的越多,消耗的內(nèi)存也就越大,最后死機)
面試官:核心線程池內(nèi)部實現(xiàn)了解嗎?
答:對于核心的幾個線程池,無論是 newFixedThreadPool() 方法,newSingleThreadExecutor() 還是 newCachedThreadPool() 方法,雖然看起來創(chuàng)建的線程有著完全不同的功能特點,但其實內(nèi)部實現(xiàn)均使用了 ThreadPoolExecutor 實現(xiàn),其實都只是 ThreadPoolExecutor 類的封裝。
為何 ThreadPoolExecutor 有如此強大的功能呢?我們可以來看一下 ThreadPoolExecutor 最重要的構(gòu)造函數(shù):
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)函數(shù)的參數(shù)含義如下:
- corePoolSize:指定了線程池中的線程數(shù)量
- maximumPoolSize:指定了線程池中的最大線程數(shù)量
- keepAliveTime:當(dāng)線程池線程數(shù)量超過 corePoolSize 時,多余的空閑線程的存活時間。即,超過了 corePoolSize 的空閑線程,在多長時間內(nèi),會被銷毀。
- unit: keepAliveTime 的單位。
- workQueue:任務(wù)隊列,被提交但尚未被執(zhí)行的任務(wù)。
- threadFactory:線程工廠,用于創(chuàng)建線程,一般用默認(rèn)的即可。
- handler:拒絕策略。當(dāng)任務(wù)太多來不及處理,如何拒絕任務(wù)。
7)Atomic關(guān)鍵字:
答:可以使基本數(shù)據(jù)類型以原子的方式實現(xiàn)自增自減等操作。參考博客:concurrent.atomic包下的類AtomicInteger的使用
8)創(chuàng)建線程有哪幾種方式?
答:有兩種創(chuàng)建線程的方法:一是實現(xiàn)Runnable接口,然后將它傳遞給Thread的構(gòu)造函數(shù),創(chuàng)建一個Thread對象;二是直接繼承Thread類。
面試官:兩種方式有什么區(qū)別呢?
- (1)Java中類是單繼承的,如果繼承了Thread了,該類就不能再有其他的直接父類了.
- (2)從操作上分析,繼承方式更簡單,獲取線程名字也簡單.(操作上,更簡單)
- (3)從多線程共享同一個資源上分析,繼承方式不能做到.
- (1)Java中類可以多實現(xiàn)接口,此時該類還可以繼承其他類,并且還可以實現(xiàn)其他接口(設(shè)計上,更優(yōu)雅).
- (2)從操作上分析,實現(xiàn)方式稍微復(fù)雜點,獲取線程名字也比較復(fù)雜,得使用Thread.currentThread()來獲取當(dāng)前線程的引用.
- (3)從多線程共享同一個資源上分析,實現(xiàn)方式可以做到(是否共享同一個資源).
9)run() 方法和 start() 方法有什么區(qū)別?
答:start() 方法會新建一個線程并讓這個線程執(zhí)行 run() 方法;而直接調(diào)用 run() 方法知識作為一個普通的方法調(diào)用而已,它只會在當(dāng)前線程中,串行執(zhí)行 run() 中的代碼。
10)你怎么理解線程優(yōu)先級?
答:Java 中的線程可以有自己的優(yōu)先級。優(yōu)先極高的線程在競爭資源時會更有優(yōu)勢,更可能搶占資源,當(dāng)然,這只是一個概率問題。如果運行不好,高優(yōu)先級線程可能也會搶占失敗。
由于線程的優(yōu)先級調(diào)度和底層操作系統(tǒng)有密切的關(guān)系,在各個平臺上表現(xiàn)不一,并且這種優(yōu)先級產(chǎn)生的后果也可能不容易預(yù)測,無法精準(zhǔn)控制,比如一個低優(yōu)先級的線程可能一直搶占不到資源,從而始終無法運行,而產(chǎn)生饑餓(雖然優(yōu)先級低,但是也不能餓死它啊)。因此,在要求嚴(yán)格的場合,還是需要自己在應(yīng)用層解決線程調(diào)度的問題。
在 Java 中,使用 1 到 10 表示線程優(yōu)先級,一般可以使用內(nèi)置的三個靜態(tài)標(biāo)量表示:
public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10;數(shù)字越大則優(yōu)先級越高,但有效范圍在 1 到 10 之間,默認(rèn)的優(yōu)先級為 5 。
11)在 Java 中如何停止一個線程?
答:Java 提供了很豐富的 API 但沒有為停止線程提供 API 。
JDK 1.0 本來有一些像 stop(),suspend() 和 resume() 的控制方法但是由于潛在的死鎖威脅因此在后續(xù)的 JDK 版本中他們被棄用了,之后 Java API 的設(shè)計者就沒有提供一個兼容且線程安全的方法來停止任何一個線程。
當(dāng) run() 或者 call() 方法執(zhí)行完的時候線程會自動結(jié)束,如果要手動結(jié)束一個線程,你可以用 volatile 布爾變量來退出 run() 方法的循環(huán)或者是取消任務(wù)來中斷線程。
12)多線程中的忙循環(huán)是什么?
答:忙循環(huán)就是程序員用循環(huán)讓一個線程等待,不像傳統(tǒng)方法 wait(),sleep() 或yield() 它們都放棄了 CPU 控制權(quán),而忙循環(huán)不會放棄 CPU,它就是在運行一個空循環(huán)。這么做的目的是為了保留 CPU 緩存。
在多核系統(tǒng)中,一個等待線程醒來的時候可能會在另一個內(nèi)核運行,這樣會重建緩存,為了避免重建緩存和減少等待重建的時間就可以使用它了。
13)10 個線程和 2 個線程的同步代碼,哪個更容易寫?
答:從寫代碼的角度來說,兩者的復(fù)雜度是相同的,因為同步代碼與線程數(shù)量是相互獨立的。但是同步策略的選擇依賴于線程的數(shù)量,因為越多的線程意味著更大的競爭,所以你需要利用同步技術(shù),如鎖分離,這要求更復(fù)雜的代碼和專業(yè)知識。
14)你是如何調(diào)用 wait()方法的?使用 if 塊還是循環(huán)?為什么?
答:wait() 方法應(yīng)該在循環(huán)調(diào)用,因為當(dāng)線程獲取到 CPU 開始執(zhí)行的時候,其他條件可能還沒有滿足,所以在處理前,循環(huán)檢測條件是否滿足會更好。下面是一段標(biāo)準(zhǔn)的使用 wait 和 notify 方法的代碼:
// The standard idiom for using the wait method synchronized (obj) { while (condition does not hold) obj.wait(); // (Releases lock, and reacquires on wakeup) ... // Perform action appropriate to condition }參見 Effective Java 第 69 條,獲取更多關(guān)于為什么應(yīng)該在循環(huán)中來調(diào)用 wait 方法的內(nèi)容。
15)什么是多線程環(huán)境下的偽共享(false sharing)?
答:偽共享是多線程系統(tǒng)(每個處理器有自己的局部緩存)中一個眾所周知的性能問題。偽共享發(fā)生在不同處理器的上的線程對變量的修改依賴于相同的緩存行,如下圖所示:
偽共享問題很難被發(fā)現(xiàn),因為線程可能訪問完全不同的全局變量,內(nèi)存中卻碰巧在很相近的位置上。如其他諸多的并發(fā)問題,避免偽共享的最基本方式是仔細(xì)審查代碼,根據(jù)緩存行來調(diào)整你的數(shù)據(jù)結(jié)構(gòu)。
16)用 wait-notify 寫一段代碼來解決生產(chǎn)者-消費者問題?
解析:這是常考的基礎(chǔ)類型的題,只要記住在同步塊中調(diào)用 wait() 和 notify()方法,如果阻塞,通過循環(huán)來測試等待條件。
答:
import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger;/*** Java program to solve Producer Consumer problem using wait and notify* method in Java. Producer Consumer is also a popular concurrency design pattern.** @author Javin Paul*/ public class ProducerConsumerSolution {public static void main(String args[]) {Vector sharedQueue = new Vector();int size = 4;Thread prodThread = new Thread(new Producer(sharedQueue, size), "Producer");Thread consThread = new Thread(new Consumer(sharedQueue, size), "Consumer");prodThread.start();consThread.start();} }class Producer implements Runnable {private final Vector sharedQueue;private final int SIZE;public Producer(Vector sharedQueue, int size) {this.sharedQueue = sharedQueue;this.SIZE = size;}@Overridepublic void run() {for (int i = 0; i < 7; i++) {System.out.println("Produced: " + i);try {produce(i);} catch (InterruptedException ex) {Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex);}}}private void produce(int i) throws InterruptedException {// wait if queue is fullwhile (sharedQueue.size() == SIZE) {synchronized (sharedQueue) {System.out.println("Queue is full " + Thread.currentThread().getName()+ " is waiting , size: " + sharedQueue.size());sharedQueue.wait();}}// producing element and notify consumerssynchronized (sharedQueue) {sharedQueue.add(i);sharedQueue.notifyAll();}} }class Consumer implements Runnable {private final Vector sharedQueue;private final int SIZE;public Consumer(Vector sharedQueue, int size) {this.sharedQueue = sharedQueue;this.SIZE = size;}@Overridepublic void run() {while (true) {try {System.out.println("Consumed: " + consume());Thread.sleep(50);} catch (InterruptedException ex) {Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex);}}}private int consume() throws InterruptedException {// wait if queue is emptywhile (sharedQueue.isEmpty()) {synchronized (sharedQueue) {System.out.println("Queue is empty " + Thread.currentThread().getName()+ " is waiting , size: " + sharedQueue.size());sharedQueue.wait();}}// Otherwise consume element and notify waiting producersynchronized (sharedQueue) {sharedQueue.notifyAll();return (Integer) sharedQueue.remove(0);}} }Output: Produced: 0 Queue is empty Consumer is waiting , size: 0 Produced: 1 Consumed: 0 Produced: 2 Produced: 3 Produced: 4 Produced: 5 Queue is full Producer is waiting , size: 4 Consumed: 1 Produced: 6 Queue is full Producer is waiting , size: 4 Consumed: 2 Consumed: 3 Consumed: 4 Consumed: 5 Consumed: 6 Queue is empty Consumer is waiting , size: 017)用 Java 寫一個線程安全的單例模式(Singleton)?
解析:有多種方法,但重點掌握的是雙重校驗鎖。
答:
1.餓漢式單例
餓漢式單例是指在方法調(diào)用前,實例就已經(jīng)創(chuàng)建好了。下面是實現(xiàn)代碼:
public class Singleton {private static Singleton instance = new Singleton();private Singleton (){}public static Singleton getInstance() {return instance;} }2.加入 synchronized 的懶漢式單例
所謂懶漢式單例模式就是在調(diào)用的時候才去創(chuàng)建這個實例,我們在對外的創(chuàng)建實例方法上加如 synchronized 關(guān)鍵字保證其在多線程中很好的工作:
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }3.使用靜態(tài)內(nèi)部類的方式創(chuàng)建單例
這種方式利用了 classloder 的機制來保證初始化 instance 時只有一個線程,它跟餓漢式的區(qū)別是:餓漢式只要 Singleton 類被加載了,那么 instance 就會被實例化(沒有達(dá)到 lazy loading 的效果),而這種方式是 Singleton 類被加載了,instance 不一定被初始化。只有顯式通過調(diào)用 getInstance() 方法時才會顯式裝載 SingletonHoder 類,從而實例化 singleton
public class Singleton {private Singleton() {}private static class SingletonHolder {// 靜態(tài)內(nèi)部類 private static Singleton singleton = new Singleton();}public static Singleton getInstance() {return SingletonHolder.singleton;} }4.雙重校驗鎖
為了達(dá)到線程安全,又能提高代碼執(zhí)行效率,我們這里可以采用DCL的雙檢查鎖機制來完成,代碼實現(xiàn)如下:
public class Singleton { private static Singleton singleton; private Singleton() { } public static Singleton getInstance(){ if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }這種是用雙重判斷來創(chuàng)建一個單例的方法,那么我們?yōu)槭裁匆褂脙蓚€if判斷這個對象當(dāng)前是不是空的呢 ?因為當(dāng)有多個線程同時要創(chuàng)建對象的時候,多個線程有可能都停止在第一個if判斷的地方,等待鎖的釋放,然后多個線程就都創(chuàng)建了對象,這樣就不是單例模式了,所以我們要用兩個if來進(jìn)行這個對象是否存在的判斷。
5.使用 static 代碼塊實現(xiàn)單例
靜態(tài)代碼塊中的代碼在使用類的時候就已經(jīng)執(zhí)行了,所以可以應(yīng)用靜態(tài)代碼塊的這個特性的實現(xiàn)單例設(shè)計模式。
public class Singleton{ private static Singleton instance = null; private Singleton(){} static{ instance = new Singleton(); } public static Singleton getInstance() { return instance; } }6.使用枚舉數(shù)據(jù)類型實現(xiàn)單例模式
枚舉enum和靜態(tài)代碼塊的特性相似,在使用枚舉時,構(gòu)造方法會被自動調(diào)用,利用這一特性也可以實現(xiàn)單例:
public class ClassFactory{ private enum MyEnumSingleton{ singletonFactory; private MySingleton instance; private MyEnumSingleton(){//枚舉類的構(gòu)造方法在類加載是被實例化 instance = new MySingleton(); } public MySingleton getInstance(){ return instance; } } public static MySingleton getInstance(){ return MyEnumSingleton.singletonFactory.getInstance(); } }小結(jié):關(guān)于 Java 中多線程編程,線程安全等知識一直都是面試中的重點和難點,還需要熟練掌握。
總結(jié)
以上是生活随笔為你收集整理的2021最新一线互联网大厂常见高并发面试题解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【图像分割】基于pcnn脉冲神经网络结合
- 下一篇: LBP特征提取算法