- 前言:
在遨游了一番 Java Web 的世界之后,發(fā)現(xiàn)了自己的一些缺失,所以就著一篇深度好文:知名互聯(lián)網(wǎng)公司校招 Java 開發(fā)崗面試知識(shí)點(diǎn)解析 专筷,來好好的對(duì) Java 知識(shí)點(diǎn)進(jìn)行復(fù)習(xí)和學(xué)習(xí)一番,大部分內(nèi)容參照自這一篇文章,有一些自己補(bǔ)充的闷旧,也算是重新學(xué)習(xí)一下 Java 吧。
前序文章鏈接:
Java 面試知識(shí)點(diǎn)解析(一)——基礎(chǔ)知識(shí)篇
(一)高并發(fā)編程基礎(chǔ)知識(shí)
這里涉及到一些基礎(chǔ)的概念钧唐,我重新捧起了一下《實(shí)戰(zhàn) Java 高并發(fā)程序設(shè)計(jì)》這一本書忙灼,感覺到心潮澎湃,這或許就是筆者敘述功底扎實(shí)的魅力吧钝侠,喜歡该园。對(duì)于并發(fā)的基礎(chǔ)可以參照一下我之前寫過的一篇博文:Java學(xué)習(xí)筆記(4)——并發(fā)基礎(chǔ)
1)多線程和單線程的區(qū)別和聯(lián)系?
答:
在單核 CPU 中帅韧,將 CPU 分為很小的時(shí)間片里初,在每一時(shí)刻只能有一個(gè)線程在執(zhí)行,是一種微觀上輪流占用 CPU 的機(jī)制忽舟。
多線程會(huì)存在線程上下文切換双妨,會(huì)導(dǎo)致程序執(zhí)行速度變慢,即采用一個(gè)擁有兩個(gè)線程的進(jìn)程執(zhí)行所需要的時(shí)間比一個(gè)線程的進(jìn)程執(zhí)行兩次所需要的時(shí)間要多一些叮阅。
結(jié)論:即采用多線程不會(huì)提高程序的執(zhí)行速度刁品,反而會(huì)降低速度雳攘,但是對(duì)于用戶來說袁铐,可以減少用戶的響應(yīng)時(shí)間驱敲。
面試官:那使用多線程有什么優(yōu)勢耻讽?
解析:盡管面臨很多挑戰(zhàn)浑彰,多線程有一些優(yōu)點(diǎn)仍然使得它一直被使用渡处,而這些優(yōu)點(diǎn)我們應(yīng)該了解陵究。
答:
(1)資源利用率更好
想象一下僚害,一個(gè)應(yīng)用程序需要從本地文件系統(tǒng)中讀取和處理文件的情景缴饭。比方說暑劝,從磁盤讀取一個(gè)文件需要5秒,處理一個(gè)文件需要2秒颗搂。處理兩個(gè)文件則需要:
1| 5秒讀取文件A
2| 2秒處理文件A
3| 5秒讀取文件B
4| 2秒處理文件B
5| ---------------------
6| 總共需要14秒
從磁盤中讀取文件的時(shí)候担猛,大部分的CPU時(shí)間用于等待磁盤去讀取數(shù)據(jù)。在這段時(shí)間里丢氢,CPU非常的空閑傅联。它可以做一些別的事情。通過改變操作的順序疚察,就能夠更好的使用CPU資源蒸走。看下面的順序:
1| 5秒讀取文件A
2| 5秒讀取文件B + 2秒處理文件A
3| 2秒處理文件B
4| ---------------------
5| 總共需要12秒
CPU等待第一個(gè)文件被讀取完貌嫡。然后開始讀取第二個(gè)文件比驻。當(dāng)?shù)诙募诒蛔x取的時(shí)候该溯,CPU會(huì)去處理第一個(gè)文件。記住别惦,在等待磁盤讀取文件的時(shí)候狈茉,CPU大部分時(shí)間是空閑的。
總的說來掸掸,CPU能夠在等待IO的時(shí)候做一些其他的事情氯庆。這個(gè)不一定就是磁盤IO。它也可以是網(wǎng)絡(luò)的IO扰付,或者用戶輸入堤撵。通常情況下,網(wǎng)絡(luò)和磁盤的IO比CPU和內(nèi)存的IO慢的多羽莺。
(2)程序設(shè)計(jì)在某些情況下更簡單
在單線程應(yīng)用程序中粒督,如果你想編寫程序手動(dòng)處理上面所提到的讀取和處理的順序,你必須記錄每個(gè)文件讀取和處理的狀態(tài)禽翼。相反屠橄,你可以啟動(dòng)兩個(gè)線程,每個(gè)線程處理一個(gè)文件的讀取和操作闰挡。線程會(huì)在等待磁盤讀取文件的過程中被阻塞锐墙。在等待的時(shí)候,其他的線程能夠使用CPU去處理已經(jīng)讀取完的文件长酗。其結(jié)果就是溪北,磁盤總是在繁忙地讀取不同的文件到內(nèi)存中。這會(huì)帶來磁盤和CPU利用率的提升夺脾。而且每個(gè)線程只需要記錄一個(gè)文件之拨,因此這種方式也很容易編程實(shí)現(xiàn)。
(3)程序響應(yīng)更快
有時(shí)我們會(huì)編寫一些較為復(fù)雜的代碼(這里的復(fù)雜不是說復(fù)雜的算法咧叭,而是復(fù)雜的業(yè)務(wù)邏輯)蚀乔,例如,一筆訂單的創(chuàng)建菲茬,它包括插入訂單數(shù)據(jù)吉挣、生成訂單趕快找、發(fā)送郵件通知賣家和記錄貨品銷售數(shù)量等婉弹。用戶從單擊“訂購”按鈕開始睬魂,就要等待這些操作全部完成才能看到訂購成功的結(jié)果。但是這么多業(yè)務(wù)操作镀赌,如何能夠讓其更快地完成呢氯哮?
在上面的場景中,可以使用多線程技術(shù)商佛,即將數(shù)據(jù)一致性不強(qiáng)的操作派發(fā)給其他線程處理(也可以使用消息隊(duì)列)喉钢,如生成訂單快照姆打、發(fā)送郵件等。這樣做的好處是響應(yīng)用戶請求的線程能夠盡可能快地處理完成出牧,縮短了響應(yīng)時(shí)間,提升了用戶體驗(yàn)歇盼。
多線程還有一些優(yōu)勢也顯而易見:
① 進(jìn)程之前不能共享內(nèi)存舔痕,而線程之間共享內(nèi)存(堆內(nèi)存)則很簡單。
② 系統(tǒng)創(chuàng)建進(jìn)程時(shí)需要為該進(jìn)程重新分配系統(tǒng)資源,創(chuàng)建線程則代價(jià)小很多,因此實(shí)現(xiàn)多任務(wù)并發(fā)時(shí),多線程效率更高.
③ Java語言本身內(nèi)置多線程功能的支持,而不是單純第作為底層系統(tǒng)的調(diào)度方式,從而簡化了多線程編程.
2)多線程一定快嗎豹缀?
答:不一定伯复。
比如,我們嘗試使用并行和串行來分別執(zhí)行累加的操作觀察是否并行執(zhí)行一定比串行執(zhí)行更快:
以下是我測試的結(jié)果邢笙,可以看出啸如,當(dāng)不超過1百萬的時(shí)候,并行是明顯比串行要慢的氮惯,為什么并發(fā)執(zhí)行的速度會(huì)比串行慢呢叮雳?這是因?yàn)榫€程有創(chuàng)建和上下文切換的開銷。
3)什么是同步妇汗?什么又是異步帘不?
解析:這是對(duì)多線程基礎(chǔ)知識(shí)的考察
答:同步和異步通常用來形容一次方法調(diào)用。
同步方法調(diào)用一旦開始杨箭,調(diào)用者必須等到方法返回后寞焙,才能繼續(xù)后續(xù)的行為。這就好像是我們?nèi)ド坛琴I一臺(tái)空調(diào)互婿,你看中了一臺(tái)空調(diào)捣郊,于是就跟售貨員下了單,然后售貨員就去倉庫幫你調(diào)配物品慈参,這天你熱的實(shí)在不行呛牲,就催著商家趕緊發(fā)貨,于是你就在商店里等著驮配,知道商家把你和空調(diào)都送回家侈净,一次愉快的購物才結(jié)束,這就是同步調(diào)用僧凤。
而異步方法更像是一個(gè)消息傳遞畜侦,一旦開始,方法調(diào)用就會(huì)立即返回躯保,調(diào)用者就可以繼續(xù)后續(xù)的操作旋膳。回到剛才買空調(diào)的例子途事,我們可以坐在里打開電腦验懊,在網(wǎng)上訂購一臺(tái)空調(diào)擅羞。當(dāng)你完成網(wǎng)上支付的時(shí)候,對(duì)你來說購物過程已經(jīng)結(jié)束了义图。雖然空調(diào)還沒有送到家减俏,但是你的任務(wù)都已經(jīng)完成了。商家接到你的訂單后碱工,就會(huì)加緊安排送貨娃承,當(dāng)然這一切已經(jīng)跟你無關(guān)了,你已經(jīng)支付完成怕篷,想什么就能去干什么了历筝,出去溜達(dá)幾圈都不成問題。等送貨上門的時(shí)候廊谓,接到商家電話梳猪,回家一趟簽收即可。這就是異步調(diào)用蒸痹。
面試官:那并發(fā)(Concurrency)和并行(Parallelism)的區(qū)別呢春弥?
解析:并行性和并發(fā)性是既相似又有區(qū)別的兩個(gè)概念。
答:并行性是指兩個(gè)或多個(gè)事件在同一時(shí)刻發(fā)生叠荠。而并發(fā)性是指連個(gè)或多個(gè)事件在同一時(shí)間間隔內(nèi)發(fā)生惕稻。
在多道程序環(huán)境下,并發(fā)性是指在一段時(shí)間內(nèi)宏觀上有多個(gè)程序在同時(shí)運(yùn)行蝙叛,但在單處理機(jī)環(huán)境下(一個(gè)處理器)俺祠,每一時(shí)刻卻僅能有一道程序執(zhí)行,故微觀上這些程序只能是分時(shí)地交替執(zhí)行借帘。例如蜘渣,在1秒鐘時(shí)間內(nèi),0-15ms程序A運(yùn)行肺然;15-30ms程序B運(yùn)行蔫缸;30-45ms程序C運(yùn)行;45-60ms程序D運(yùn)行际起,因此可以說拾碌,在1秒鐘時(shí)間間隔內(nèi),宏觀上有四道程序在同時(shí)運(yùn)行街望,但微觀上校翔,程序A、B灾前、C防症、D是分時(shí)地交替執(zhí)行的。
如果在計(jì)算機(jī)系統(tǒng)中有多個(gè)處理機(jī),這些可以并發(fā)執(zhí)行的程序就可以被分配到多個(gè)處理機(jī)上蔫敲,實(shí)現(xiàn)并發(fā)執(zhí)行饲嗽,即利用每個(gè)處理機(jī)處理一個(gè)可并發(fā)執(zhí)行的程序。這樣奈嘿,多個(gè)程序便可以同時(shí)執(zhí)行貌虾。以此就能提高系統(tǒng)中的資源利用率,增加系統(tǒng)的吞吐量裙犹。
4)線程和進(jìn)程的區(qū)別:(必考)
答:
- 進(jìn)程是一個(gè) “執(zhí)行中的程序”尽狠,是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位;
- 線程是進(jìn)程的一個(gè)實(shí)體伯诬,一個(gè)進(jìn)程中擁有多個(gè)線程晚唇,線程之間共享地址空間和其它資源(所以通信和同步等操作線程比進(jìn)程更加容易)巫财;
- 線程上下文的切換比進(jìn)程上下文切換要快很多盗似。
(1)進(jìn)程切換時(shí),涉及到當(dāng)前進(jìn)程的 CPU 環(huán)境的保存和新被調(diào)度運(yùn)行進(jìn)程的 CPU 環(huán)境的設(shè)置平项。
(2)線程切換僅需要保存和設(shè)置少量的寄存器內(nèi)容赫舒,不涉及存儲(chǔ)管理方面的操作。
面試官:進(jìn)程間如何通訊闽瓢?線程間如何通訊接癌?
答:進(jìn)程間通訊依靠 IPC 資源,例如管道(pipes)扣讼、套接字(sockets)等缺猛;
線程間通訊依靠 JVM 提供的 API,例如 wait()椭符、notify()荔燎、notifyAll() 等方法,線程間還可以通過共享的主內(nèi)存來進(jìn)行值的傳遞销钝。
關(guān)于線程和進(jìn)程有一篇寫得非常不錯(cuò)的文章有咨,不過是英文的,我進(jìn)行了翻譯蒸健,相信閱讀之后會(huì)對(duì)進(jìn)程和線程有不一樣的理解:線程和進(jìn)程基礎(chǔ)——翻譯文
5)什么是阻塞(Blocking)和非阻塞(Non-Blocking)座享?
答:阻塞和非阻塞通常用來形容多線程間的相互影響。比如一個(gè)線程占用了臨界區(qū)資源似忧,那么其他所有需要這個(gè)而資源的線程就必須在這個(gè)臨界區(qū)中進(jìn)行等待渣叛。等待會(huì)導(dǎo)致線程掛起,這種情況就是阻塞盯捌。此時(shí)诗箍,如果占用資源的線程一直不愿意釋放資源,那么其他所有阻塞在這個(gè)臨界區(qū)上的線程都不能工作。
非阻塞的意思與之相反滤祖,它強(qiáng)調(diào)沒有一個(gè)線程可以妨礙其他線程執(zhí)行筷狼。所有的線程都會(huì)嘗試不斷前向執(zhí)行。
面試官:臨界區(qū)是什么匠童?
答:臨界區(qū)用來表示一種公共資源或者說是共享資源埂材,可以被多個(gè)線程使用。但是每一次汤求,只能有一個(gè)線程使用它俏险,一旦臨界區(qū)資源被占用,其他線程要想使用這個(gè)資源扬绪,就必須等待竖独。
比如,在一個(gè)辦公室里有一臺(tái)打印機(jī)挤牛,打印機(jī)一次只能執(zhí)行一個(gè)任務(wù)莹痢。如果小王和小明同時(shí)需要打印文件,很顯然墓赴,如果小王先下發(fā)了打印任務(wù)竞膳,打印機(jī)就開始打印小王的文件了,小明的任務(wù)就只能等待小王打印結(jié)束后才能打印诫硕,這里的打印機(jī)就是一個(gè)臨界區(qū)的例子坦辟。
在并行程序中,臨界區(qū)資源是保護(hù)的對(duì)象章办,如果意外出現(xiàn)打印機(jī)同時(shí)執(zhí)行兩個(gè)打印任務(wù)锉走,那么最可能的結(jié)果就是打印出來的文件就會(huì)是損壞的文件,它既不是小王想要的藕届,也不是小明想要的挪蹭。
6)什么是死鎖(Deadlock)、饑餓(Starvation)和活鎖(Livelock)翰舌?
答:死鎖嚣潜、饑餓和活鎖都屬于多線程的活躍性問題,如果發(fā)現(xiàn)上述幾種情況椅贱,那么相關(guān)線程可能就不再活躍懂算,也就說它可能很難再繼續(xù)往下執(zhí)行了。
死鎖應(yīng)該是最糟糕的一種情況了庇麦,它表示兩個(gè)或者兩個(gè)以上的進(jìn)程在執(zhí)行過程中计技,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無外力作用山橄,它們都將無法推進(jìn)下去垮媒。此時(shí)稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖,這些永遠(yuǎn)在互相等待的進(jìn)程稱為死鎖進(jìn)程。
饑餓是指某一個(gè)或者多個(gè)線程因?yàn)榉N種原因無法獲得所需要的資源睡雇,導(dǎo)致一直無法執(zhí)行萌衬。比如:
1)它的線程優(yōu)先級(jí)可能太低,而高優(yōu)先級(jí)的線程不斷搶占它需要的資源它抱,導(dǎo)致低優(yōu)先級(jí)的線程無法工作秕豫。在自然界中,母雞喂食雛鳥時(shí)观蓄,很容易出現(xiàn)這種情況混移,由于雛鳥很多,食物有限侮穿,雛鳥之間的食物競爭可能非常厲害歌径,小雛鳥因?yàn)榻?jīng)常搶不到食物,有可能會(huì)被餓死亲茅。線程的饑餓也非常類似這種情況回铛。
2)另外一種可能是,某一個(gè)線程一直占著關(guān)鍵資源不放芯急,導(dǎo)致其他需要這個(gè)資源的線程無法正常執(zhí)行勺届,這種情況也是饑餓的一種驶俊。
與死鎖相比娶耍,饑餓還是有可能在未來一段時(shí)間內(nèi)解決的(比如高優(yōu)先級(jí)的線程已經(jīng)完成任務(wù),不再瘋狂的執(zhí)行)活鎖是一種非常有趣的情況饼酿。不知道大家是不是有遇到過這樣一種情況榕酒,當(dāng)你要坐電梯下樓,電梯到了故俐,門開了想鹰,這時(shí)你正準(zhǔn)備出去,但不巧的是药版,門外一個(gè)人擋著你的去路辑舷,他想進(jìn)來。于是你很紳士的靠左走槽片,避讓對(duì)方何缓,但同時(shí)對(duì)方也很紳士,但他靠右走希望避讓你还栓。結(jié)果碌廓,你們又撞上了。于是乎剩盒,你們都意識(shí)到了問題谷婆,希望盡快避讓對(duì)方,你立即向右走,他也立即向左走纪挎,結(jié)果又撞上了期贫!不過介于人類的只能,我相信這個(gè)動(dòng)作重復(fù) 2异袄、 3 次后唯灵,你應(yīng)該可以順利解決這個(gè)問題,因?yàn)檫@個(gè)時(shí)候隙轻,大家都會(huì)本能的對(duì)視埠帕,進(jìn)行交流,保證這種情況不再發(fā)生玖绿。
但如果這種情況發(fā)生在兩個(gè)線程間可能就不會(huì)那么幸運(yùn)了敛瓷,如果線程的智力不夠,且都秉承著 “謙讓” 的原則斑匪,主動(dòng)將資源釋放給他人使用呐籽,那么就會(huì)出現(xiàn)資源不斷在兩個(gè)線程中跳動(dòng),而沒有一個(gè)線程可以同時(shí)拿到所有的資源而正常執(zhí)行蚀瘸。這種情況就是活鎖狡蝶。
7)多線程產(chǎn)生死鎖的 4 個(gè)必要條件?
答:
互斥條件:一個(gè)資源每次只能被一個(gè)線程使用贮勃;
請求與保持條件:一個(gè)線程因請求資源而阻塞時(shí)贪惹,對(duì)已獲得的資源保持不放;
不剝奪條件:進(jìn)程已經(jīng)獲得的資源寂嘉,在未使用完之前奏瞬,不能強(qiáng)行剝奪;
循環(huán)等待條件:若干線程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系泉孩。
面試官:如何避免死鎖硼端?(經(jīng)常接著問這個(gè)問題哦~)
答:指定獲取鎖的順序,舉例如下:
比如某個(gè)線程只有獲得 A 鎖和 B 鎖才能對(duì)某資源進(jìn)行操作寓搬,在多線程條件下珍昨,如何避免死鎖?
獲得鎖的順序是一定的句喷,比如規(guī)定镣典,只有獲得 A 鎖的線程才有資格獲取 B 鎖,按順序獲取鎖就可以避免死鎖T嗳隆B嫫病!
8)如何指定多個(gè)線程的執(zhí)行順序父叙?
解析:面試官會(huì)給你舉個(gè)例子神郊,如何讓 10 個(gè)線程按照順序打印 0123456789肴裙?(寫代碼實(shí)現(xiàn))
答:
設(shè)定一個(gè) orderNum,每個(gè)線程執(zhí)行結(jié)束之后涌乳,更新 orderNum蜻懦,指明下一個(gè)要執(zhí)行的線程。并且喚醒所有的等待線程夕晓。
在每一個(gè)線程的開始宛乃,要 while 判斷 orderNum 是否等于自己的要求值!蒸辆!不是征炼,則 wait,是則執(zhí)行本線程躬贡。
9)Java 中線程有幾種狀態(tài)谆奥?
答:六種(查看 Java 源碼也可以看到是 6 種),并且某個(gè)時(shí)刻 Java 線程只能處于其中的一個(gè)狀態(tài)拂玻。
新建(NEW)狀態(tài):表示新創(chuàng)建了一個(gè)線程對(duì)象酸些,而此時(shí)線程并沒有開始執(zhí)行。
可運(yùn)行(RUNNABLE)狀態(tài):線程對(duì)象創(chuàng)建后檐蚜,其它線程(比如 main 線程)調(diào)用了該對(duì)象的 start() 方法魄懂,才表示線程開始執(zhí)行。當(dāng)線程執(zhí)行時(shí)闯第,處于 RUNNBALE 狀態(tài)市栗,表示線程所需的一切資源都已經(jīng)準(zhǔn)備好了。該狀態(tài)的線程位于可運(yùn)行線程池中乡括,等待被線程調(diào)度選中肃廓,獲取 cpu 的使用權(quán)智厌。
阻塞(BLOCKED)狀態(tài):如果線程在執(zhí)行過程終于到了 synchronized 同步塊诲泌,就會(huì)進(jìn)入 BLOCKED 阻塞狀態(tài),這時(shí)線程就會(huì)暫停執(zhí)行铣鹏,直到獲得請求的鎖敷扫。
等待(WAITING)狀態(tài):當(dāng)線程等待另一個(gè)線程通知調(diào)度器一個(gè)條件時(shí),它自己進(jìn)入等待狀態(tài)诚卸。在調(diào)用Object.wait方法或Thread.join方法葵第,或者是等待java.util.concurrent庫中的Lock或Condition時(shí),就會(huì)出現(xiàn)這種情況合溺;
計(jì)時(shí)等待(TIMED_WAITING)狀態(tài):Object.wait卒密、Thread.join、Lock.tryLock和Condition.await 等方法有超時(shí)參數(shù)棠赛,還有 Thread.sleep 方法哮奇、LockSupport.parkNanos 方法和 LockSupport.parkUntil 方法膛腐,這些方法會(huì)導(dǎo)致線程進(jìn)入計(jì)時(shí)等待狀態(tài),如果超時(shí)或者出現(xiàn)通知鼎俘,都會(huì)切換會(huì)可運(yù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ā)編程中很常用的實(shí)用工具類巾钉,用于定義類似于線程的自定義子系統(tǒng),包括線程池秘案、異步 IO 和輕量級(jí)任務(wù)框架砰苍。
1)sleep( ) 和 wait( n)、wait( ) 的區(qū)別:
答:
sleep 方法:是 Thread 類的靜態(tài)方法阱高,當(dāng)前線程將睡眠 n 毫秒赚导,線程進(jìn)入阻塞狀態(tài)。當(dāng)睡眠時(shí)間到了赤惊,會(huì)解除阻塞吼旧,進(jìn)行可運(yùn)行狀態(tài),等待 CPU 的到來未舟。睡眠不釋放鎖(如果有的話)圈暗;
wait 方法:是 Object 的方法,必須與 synchronized 關(guān)鍵字一起使用裕膀,線程進(jìn)入阻塞狀態(tài)员串,當(dāng) notify 或者 notifyall 被調(diào)用后,會(huì)解除阻塞昼扛。但是寸齐,只有重新占用互斥鎖之后才會(huì)進(jìn)入可運(yùn)行狀態(tài)。睡眠時(shí)抄谐,釋放互斥鎖渺鹦。
2)synchronized 關(guān)鍵字:
答:底層實(shí)現(xiàn):
進(jìn)入時(shí),執(zhí)行 monitorenter蛹含,將計(jì)數(shù)器 +1毅厚,釋放鎖 monitorexit 時(shí),計(jì)數(shù)器-1浦箱;
當(dāng)一個(gè)線程判斷到計(jì)數(shù)器為 0 時(shí)吸耿,則當(dāng)前鎖空閑殴边,可以占用;反之珍语,當(dāng)前線程進(jìn)入等待狀態(tài)锤岸。
含義:(monitor 機(jī)制)
Synchronized 是在加鎖,加對(duì)象鎖板乙。對(duì)象鎖是一種重量鎖(monitor)是偷,synchronized 的鎖機(jī)制會(huì)根據(jù)線程競爭情況在運(yùn)行時(shí)會(huì)有偏向鎖(單一線程)、輕量鎖(多個(gè)線程訪問 synchronized 區(qū)域)募逞、對(duì)象鎖(重量鎖蛋铆,多個(gè)線程存在競爭的情況)、自旋鎖等放接。
該關(guān)鍵字是一個(gè)幾種鎖的封裝刺啦。
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 能使得一個(gè)非原子操作變成原子操作嗎渺绒?
答:能。
一個(gè)典型的例子是在類中有一個(gè) long 類型的成員變量菱鸥。如果你知道該成員變量會(huì)被多個(gè)線程訪問宗兼,如計(jì)數(shù)器、價(jià)格等氮采,你最好是將其設(shè)置為 volatile殷绍。為什么?因?yàn)?Java 中讀取 long 類型變量不是原子的扳抽,需要分成兩步篡帕,如果一個(gè)線程正在修改該 long 變量的值,另一個(gè)線程可能只能看到該值的一半(前 32 位)贸呢。但是對(duì)一個(gè) volatile 型的 long 或 double 變量的讀寫是原子。
面試官:volatile 修飾符的有過什么實(shí)踐拢军?
答:
一種實(shí)踐是用 volatile 修飾 long 和 double 變量楞陷,使其能按原子類型來讀寫。double 和 long 都是64位寬茉唉,因此對(duì)這兩種類型的讀是分為兩部分的固蛾,第一次讀取第一個(gè) 32 位,然后再讀剩下的 32 位,這個(gè)過程不是原子的亮蒋,但 Java 中 volatile 型的 long 或 double 變量的讀寫是原子的煮寡。
volatile 修復(fù)符的另一個(gè)作用是提供內(nèi)存屏障(memory barrier),例如在分布式框架中的應(yīng)用趾诗。簡單的說蜡感,就是當(dāng)你寫一個(gè) volatile 變量之前,Java 內(nèi)存模型會(huì)插入一個(gè)寫屏障(write barrier)恃泪,讀一個(gè) volatile 變量之前郑兴,會(huì)插入一個(gè)讀屏障(read barrier)。意思就是說贝乎,在你寫一個(gè) volatile 域時(shí)情连,能保證任何線程都能看到你寫的值,同時(shí)览效,在寫之前却舀,也能保證任何數(shù)值的更新對(duì)所有線程是可見的,因?yàn)閮?nèi)存屏障會(huì)將其他所有寫的值更新到緩存锤灿。
5)ThreadLocal(線程局部變量)關(guān)鍵字:
答:當(dāng)使用 ThreadLocal 維護(hù)變量時(shí)禁筏,其為每個(gè)使用該變量的線程提供獨(dú)立的變量副本,所以每一個(gè)線程都可以獨(dú)立的改變自己的副本衡招,而不會(huì)影響其他線程對(duì)應(yīng)的副本篱昔。
ThreadLocal 內(nèi)部實(shí)現(xiàn)機(jī)制:
每個(gè)線程內(nèi)部都會(huì)維護(hù)一個(gè)類似 HashMap 的對(duì)象,稱為 ThreadLocalMap始腾,里邊會(huì)包含若干了 Entry(K-V 鍵值對(duì))州刽,相應(yīng)的線程被稱為這些 Entry 的屬主線程;
Entry 的 Key 是一個(gè) ThreadLocal 實(shí)例浪箭,Value 是一個(gè)線程特有對(duì)象穗椅。Entry 的作用即是:為其屬主線程建立起一個(gè) ThreadLocal 實(shí)例與一個(gè)線程特有對(duì)象之間的對(duì)應(yīng)關(guān)系;
Entry 對(duì) Key 的引用是弱引用奶栖;Entry 對(duì) Value 的引用是強(qiáng)引用匹表。
6)線程池有了解嗎?(必考)
答:java.util.concurrent.ThreadPoolExecutor 類就是一個(gè)線程池宣鄙∨鄱疲客戶端調(diào)用 ThreadPoolExecutor.submit(Runnable task) 提交任務(wù),線程池內(nèi)部維護(hù)的工作者線程的數(shù)量就是該線程池的線程池大小冻晤,有 3 種形態(tài):
- 當(dāng)前線程池大小 :表示線程池中實(shí)際工作者線程的數(shù)量苇羡;
- 最大線程池大小 (maxinumPoolSize):表示線程池中允許存在的工作者線程的數(shù)量上限;
- 核心線程大小 (corePoolSize ):表示一個(gè)不大于最大線程池大小的工作者線程數(shù)量上限鼻弧。
如果運(yùn)行的線程少于 corePoolSize设江,則 Executor 始終首選添加新的線程锦茁,而不進(jìn)行排隊(duì);
如果運(yùn)行的線程等于或者多于 corePoolSize叉存,則 Executor 始終首選將請求加入隊(duì)列码俩,而不是添加新線程;
如果無法將請求加入隊(duì)列歼捏,即隊(duì)列已經(jīng)滿了稿存,則創(chuàng)建新的線程,除非創(chuàng)建此線程超出 maxinumPoolSize甫菠, 在這種情況下挠铲,任務(wù)將被拒絕。
面試官:我們?yōu)槭裁匆褂镁€程池寂诱?
答:
減少創(chuàng)建和銷毀線程的次數(shù)拂苹,每個(gè)工作線程都可以被重復(fù)利用,可執(zhí)行多個(gè)任務(wù)痰洒。
可以根據(jù)系統(tǒng)的承受能力瓢棒,調(diào)整線程池中工作線程的數(shù)目,放置因?yàn)橄倪^多的內(nèi)存丘喻,而把服務(wù)器累趴下(每個(gè)線程大約需要 1 MB 內(nèi)存脯宿,線程開的越多,消耗的內(nèi)存也就越大泉粉,最后死機(jī))
面試官:核心線程池內(nèi)部實(shí)現(xiàn)了解嗎连霉?
答:對(duì)于核心的幾個(gè)線程池,無論是 newFixedThreadPool() 方法嗡靡,newSingleThreadExecutor() 還是 newCachedThreadPool() 方法跺撼,雖然看起來創(chuàng)建的線程有著完全不同的功能特點(diǎn),但其實(shí)內(nèi)部實(shí)現(xiàn)均使用了 ThreadPoolExecutor 實(shí)現(xiàn)讨彼,其實(shí)都只是 ThreadPoolExecutor 類的封裝歉井。
為何 ThreadPoolExecutor 有如此強(qiáng)大的功能呢?我們可以來看一下 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 時(shí)哈误,多余的空閑線程的存活時(shí)間哩至。即,超過了 corePoolSize 的空閑線程蜜自,在多長時(shí)間內(nèi)菩貌,會(huì)被銷毀。
- unit: keepAliveTime 的單位袁辈。
- workQueue:任務(wù)隊(duì)列菜谣,被提交但尚未被執(zhí)行的任務(wù)。
- threadFactory:線程工廠晚缩,用于創(chuàng)建線程尾膊,一般用默認(rèn)的即可。
- handler:拒絕策略荞彼。當(dāng)任務(wù)太多來不及處理冈敛,如何拒絕任務(wù)。
7)Atomic關(guān)鍵字:
答:可以使基本數(shù)據(jù)類型以原子的方式實(shí)現(xiàn)自增自減等操作鸣皂。參考博客:concurrent.atomic包下的類AtomicInteger的使用
8)創(chuàng)建線程有哪幾種方式抓谴?
答:有兩種創(chuàng)建線程的方法:一是實(shí)現(xiàn)Runnable接口,然后將它傳遞給Thread的構(gòu)造函數(shù)寞缝,創(chuàng)建一個(gè)Thread對(duì)象;二是直接繼承Thread類癌压。
面試官:兩種方式有什么區(qū)別呢?
- 繼承方式:
- (1)Java中類是單繼承的,如果繼承了Thread了,該類就不能再有其他的直接父類了.
- (2)從操作上分析,繼承方式更簡單,獲取線程名字也簡單.(操作上,更簡單)
- (3)從多線程共享同一個(gè)資源上分析,繼承方式不能做到.
- 實(shí)現(xiàn)方式:
- (1)Java中類可以多實(shí)現(xiàn)接口,此時(shí)該類還可以繼承其他類,并且還可以實(shí)現(xiàn)其他接口(設(shè)計(jì)上,更優(yōu)雅).
- (2)從操作上分析,實(shí)現(xiàn)方式稍微復(fù)雜點(diǎn),獲取線程名字也比較復(fù)雜,得使用Thread.currentThread()來獲取當(dāng)前線程的引用.
- (3)從多線程共享同一個(gè)資源上分析,實(shí)現(xiàn)方式可以做到(是否共享同一個(gè)資源).
9)run() 方法和 start() 方法有什么區(qū)別荆陆?
答:start() 方法會(huì)新建一個(gè)線程并讓這個(gè)線程執(zhí)行 run() 方法滩届;而直接調(diào)用 run() 方法知識(shí)作為一個(gè)普通的方法調(diào)用而已,它只會(huì)在當(dāng)前線程中被啼,串行執(zhí)行 run() 中的代碼帜消。
10)你怎么理解線程優(yōu)先級(jí)?
答:Java 中的線程可以有自己的優(yōu)先級(jí)浓体。優(yōu)先極高的線程在競爭資源時(shí)會(huì)更有優(yōu)勢泡挺,更可能搶占資源,當(dāng)然命浴,這只是一個(gè)概率問題娄猫。如果運(yùn)行不好,高優(yōu)先級(jí)線程可能也會(huì)搶占失敗生闲。
由于線程的優(yōu)先級(jí)調(diào)度和底層操作系統(tǒng)有密切的關(guān)系媳溺,在各個(gè)平臺(tái)上表現(xiàn)不一,并且這種優(yōu)先級(jí)產(chǎn)生的后果也可能不容易預(yù)測跪腹,無法精準(zhǔn)控制褂删,比如一個(gè)低優(yōu)先級(jí)的線程可能一直搶占不到資源,從而始終無法運(yùn)行冲茸,而產(chǎn)生饑餓(雖然優(yōu)先級(jí)低屯阀,但是也不能餓死它啊)轴术。因此难衰,在要求嚴(yán)格的場合,還是需要自己在應(yīng)用層解決線程調(diào)度的問題逗栽。
在 Java 中盖袭,使用 1 到 10 表示線程優(yōu)先級(jí),一般可以使用內(nèi)置的三個(gè)靜態(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)先級(jí)越高,但有效范圍在 1 到 10 之間鳄虱,默認(rèn)的優(yōu)先級(jí)為 5 弟塞。
11)在 Java 中如何停止一個(gè)線程?
答:Java 提供了很豐富的 API 但沒有為停止線程提供 API 拙已。
JDK 1.0 本來有一些像 stop()决记,suspend() 和 resume() 的控制方法但是由于潛在的死鎖威脅因此在后續(xù)的 JDK 版本中他們被棄用了,之后 Java API 的設(shè)計(jì)者就沒有提供一個(gè)兼容且線程安全的方法來停止任何一個(gè)線程倍踪。
當(dāng) run() 或者 call() 方法執(zhí)行完的時(shí)候線程會(huì)自動(dòng)結(jié)束系宫,如果要手動(dòng)結(jié)束一個(gè)線程,你可以用 volatile 布爾變量來退出 run() 方法的循環(huán)或者是取消任務(wù)來中斷線程建车。
12)多線程中的忙循環(huán)是什么扩借?
答:忙循環(huán)就是程序員用循環(huán)讓一個(gè)線程等待,不像傳統(tǒng)方法 wait(),sleep() 或yield() 它們都放棄了 CPU 控制權(quán)缤至,而忙循環(huán)不會(huì)放棄 CPU潮罪,它就是在運(yùn)行一個(gè)空循環(huán)。這么做的目的是為了保留 CPU 緩存凄杯。
在多核系統(tǒng)中错洁,一個(gè)等待線程醒來的時(shí)候可能會(huì)在另一個(gè)內(nèi)核運(yùn)行,這樣會(huì)重建緩存戒突,為了避免重建緩存和減少等待重建的時(shí)間就可以使用它了屯碴。
13)10 個(gè)線程和 2 個(gè)線程的同步代碼,哪個(gè)更容易寫膊存?
答:從寫代碼的角度來說导而,兩者的復(fù)雜度是相同的,因?yàn)橥酱a與線程數(shù)量是相互獨(dú)立的隔崎。但是同步策略的選擇依賴于線程的數(shù)量今艺,因?yàn)樵蕉嗟木€程意味著更大的競爭,所以你需要利用同步技術(shù)爵卒,如鎖分離虚缎,這要求更復(fù)雜的代碼和專業(yè)知識(shí)。
14)你是如何調(diào)用 wait()方法的钓株?使用 if 塊還是循環(huán)实牡?為什么?
答:wait() 方法應(yīng)該在循環(huán)調(diào)用轴合,因?yàn)楫?dāng)線程獲取到 CPU 開始執(zhí)行的時(shí)候创坞,其他條件可能還沒有滿足,所以在處理前受葛,循環(huán)檢測條件是否滿足會(huì)更好题涨。下面是一段標(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)(每個(gè)處理器有自己的局部緩存)中一個(gè)眾所周知的性能問題巡雨。偽共享發(fā)生在不同處理器的上的線程對(duì)變量的修改依賴于相同的緩存行,如下圖所示:
偽共享問題很難被發(fā)現(xiàn)婉支,因?yàn)榫€程可能訪問完全不同的全局變量鸯隅,內(nèi)存中卻碰巧在很相近的位置上澜建。如其他諸多的并發(fā)問題向挖,避免偽共享的最基本方式是仔細(xì)審查代碼,根據(jù)緩存行來調(diào)整你的數(shù)據(jù)結(jié)構(gòu)炕舵。
16)用 wait-notify 寫一段代碼來解決生產(chǎn)者-消費(fèi)者問題何之?
解析:這是常考的基礎(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;
}
@Override
public 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 full
while (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 consumers
synchronized (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;
}
@Override
public 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 empty
while (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 producer
synchronized (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: 0
17)用 Java 寫一個(gè)線程安全的單例模式(Singleton)蒜危?
解析:有多種方法,但重點(diǎn)掌握的是雙重校驗(yàn)鎖睹耐。
答:
1.餓漢式單例
餓漢式單例是指在方法調(diào)用前辐赞,實(shí)例就已經(jīng)創(chuàng)建好了。下面是實(shí)現(xiàn)代碼:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
2.加入 synchronized 的懶漢式單例
所謂懶漢式單例模式就是在調(diào)用的時(shí)候才去創(chuàng)建這個(gè)實(shí)例硝训,我們在對(duì)外的創(chuàng)建實(shí)例方法上加如 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 的機(jī)制來保證初始化 instance 時(shí)只有一個(gè)線程响委,它跟餓漢式的區(qū)別是:餓漢式只要 Singleton 類被加載了,那么 instance 就會(huì)被實(shí)例化(沒有達(dá)到 lazy loading 的效果)窖梁,而這種方式是 Singleton 類被加載了赘风,instance 不一定被初始化。只有顯式通過調(diào)用 getInstance() 方法時(shí)才會(huì)顯式裝載 SingletonHoder 類纵刘,從而實(shí)例化 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.雙重校驗(yàn)鎖
為了達(dá)到線程安全邀窃,又能提高代碼執(zhí)行效率,我們這里可以采用DCL的雙檢查鎖機(jī)制來完成假哎,代碼實(shí)現(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)建一個(gè)單例的方法瞬捕,那么我們?yōu)槭裁匆褂脙蓚€(gè)if判斷這個(gè)對(duì)象當(dāng)前是不是空的呢 ?因?yàn)楫?dāng)有多個(gè)線程同時(shí)要?jiǎng)?chuàng)建對(duì)象的時(shí)候位谋,多個(gè)線程有可能都停止在第一個(gè)if判斷的地方山析,等待鎖的釋放,然后多個(gè)線程就都創(chuàng)建了對(duì)象掏父,這樣就不是單例模式了笋轨,所以我們要用兩個(gè)if來進(jìn)行這個(gè)對(duì)象是否存在的判斷。
5.使用 static 代碼塊實(shí)現(xiàn)單例
靜態(tài)代碼塊中的代碼在使用類的時(shí)候就已經(jīng)執(zhí)行了,所以可以應(yīng)用靜態(tài)代碼塊的這個(gè)特性的實(shí)現(xiàn)單例設(shè)計(jì)模式爵政。
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
static{
instance = new Singleton();
}
public static Singleton getInstance() {
return instance;
}
}
6.使用枚舉數(shù)據(jù)類型實(shí)現(xiàn)單例模式
枚舉enum和靜態(tài)代碼塊的特性相似仅讽,在使用枚舉時(shí),構(gòu)造方法會(huì)被自動(dòng)調(diào)用钾挟,利用這一特性也可以實(shí)現(xiàn)單例:
public class ClassFactory{
private enum MyEnumSingleton{
singletonFactory;
private MySingleton instance;
private MyEnumSingleton(){//枚舉類的構(gòu)造方法在類加載是被實(shí)例化
instance = new MySingleton();
}
public MySingleton getInstance(){
return instance;
}
}
public static MySingleton getInstance(){
return MyEnumSingleton.singletonFactory.getInstance();
}
}
小結(jié):關(guān)于 Java 中多線程編程洁灵,線程安全等知識(shí)一直都是面試中的重點(diǎn)和難點(diǎn),還需要熟練掌握掺出。
參考資料:
① 知名互聯(lián)網(wǎng)公司校招 Java 開發(fā)崗面試知識(shí)點(diǎn)解析
② 最近5年133個(gè)Java面試問題列表
③ 《實(shí)戰(zhàn) Java 高并發(fā)程序設(shè)計(jì) —— 葛一鳴 郭超 編著》
歡迎轉(zhuǎn)載徽千,轉(zhuǎn)載請注明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關(guān)注公眾微信號(hào):wmyskxz_javaweb
分享自己的Java Web學(xué)習(xí)之路以及各種Java學(xué)習(xí)資料