Java面試系列之并發(fā)編程專題-Synchronized靈魂拷問
作者:
修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。
金三銀四跳槽季即將來臨,想必有些猿友已經(jīng)蠢蠢欲動在做相關(guān)的準(zhǔn)備了!在接下來的日子里,筆者將堅持寫作、分享Java工程師在面試求職期間的方方面面,包括簡歷制作、面試場景復(fù)現(xiàn)、面試題解答、談薪技巧 以及 項目的實戰(zhàn)!今天我們先拿Java里面的并發(fā)編程之Synchronized來開刀!
以下內(nèi)容來自 程序員實戰(zhàn)基地 fightjava.com 一位網(wǎng)友最近的面試場景,筆者嘗試著將其復(fù)現(xiàn),話不多說,咱們直接開擼!
1. 面試官:Synchronized有用過嗎?談?wù)勀銓λ睦斫?span lang="EN-US">
(1)畫外音:面試官主要是想了解你有沒有Java并發(fā)編程方面的經(jīng)驗,可以講講它的概念和部分原理!
(2)回答:Synchronized是Java的關(guān)鍵詞,JVM實現(xiàn)的一種可以實現(xiàn)并發(fā)產(chǎn)生的多個線程互斥同步訪問共享資源的 方式,也可以說是一種 “同步互斥鎖”,在實際代碼中可用于修飾代碼塊、方法、靜態(tài)方法以及類;適用于單體應(yīng)用系統(tǒng)架構(gòu)
2.面試官:嗯,說一說它的原理?
(1)畫外音:這么快就問原理,看來是動真格的了,不是隨便問問而已!
(2)回答: 通過查看被Synchronized 修飾過的代碼塊編譯后的字節(jié)碼,會發(fā)現(xiàn)編譯器會在 被Synchronized修飾過的代碼塊 的前、后生成兩個字節(jié)碼指令:monitorenter、monitorexit;
這兩個字節(jié)碼指令的含義:當(dāng)JVM執(zhí)行到monitorenter指令時,首先會嘗試著先獲取對象(共享資源)的鎖,如果該對象沒有被鎖定、又或者當(dāng)前線程已經(jīng)擁有了這個對象的鎖時,則鎖的計數(shù)器count加1,即執(zhí)行 +1 操作;當(dāng)JVM執(zhí)行monitorexit指令時,則將鎖的計數(shù)器count減一,即執(zhí)行 -1 操作;
當(dāng)計數(shù)器count為0時 ,該對象的鎖就被釋放了??!
如果當(dāng)前線程獲取該對象的鎖失敗了,則進入堵塞等待狀態(tài),直到該對象的鎖被另外一個線程釋放為止;即Java中的Synchronize底層其實是通過對象(共享資源)頭、尾設(shè)置標(biāo)記,從而實現(xiàn)鎖的獲取和釋放。
3.面試官:你剛才提到獲取對象的鎖,說一說“鎖”到底是什么,如何確定對象的鎖?
(1)畫外音:這是筆者自行想象、擴充的內(nèi)容!
(2)回答: “鎖” 可以理解為monitorenter和monitorexit字節(jié)碼指令之間的一個 Reference類型的參數(shù),即要鎖定Lock和解鎖UnLock的對象
眾所周知,使用Synchronized可以修飾不同的對象,因此,對應(yīng)的對象的鎖可以這么確定:
A.如果Synchronized 明確指定了“鎖”的對象,比如Synchronized變量、 Synchronized(this) 等,說明加、解鎖的即為該變量、當(dāng)前對象;
B.若 Synchronized 修飾的方法為非靜態(tài)方法,表示此方法對應(yīng)的對象為“鎖”對象; 若 Synchronized 修飾的方法為靜態(tài)方法,則表示此方法對應(yīng)的類對象為“鎖”對象;
注意:當(dāng)一個對象被鎖住時,對象里面所有用Synchronized 修飾的方法都將產(chǎn)生堵塞, 而對象里非Synchronized 修飾的方法可正常被調(diào)用,不受鎖的影響;
4.面試官:什么叫可重入鎖,為什么說Synchronized是可重入鎖?
(1)畫外音:這面試官腦袋瓜轉(zhuǎn)得可真快!
(2)回答:通俗地講,“可重入”指的是:當(dāng) 當(dāng)前線程獲取到了當(dāng)前對象的鎖之后,如果后續(xù)的操作仍然需要獲取獲取該對象的鎖時,可以不用再次重新獲取,即可以直接操作該對象(共享資源);
可重入性是鎖的一個基本要求,是為了解決自己鎖死自己的情況,比如一個類的同步方法調(diào)用另一個同步方法時,假如Synchronized不支持重入,進入method2方法時當(dāng)前線程已經(jīng)獲得鎖,而在method2方法里面執(zhí)行method1時當(dāng)前線程又要去嘗試獲取鎖,這時如果不支持重入,它就要等待釋放,把自己阻塞,導(dǎo)致很有可能自己鎖死自己!
對Synchronized來說,可重入性是顯而易見的,剛才提到,在執(zhí)行monitorenter指令時,如果這個對象沒有鎖定,或者當(dāng)前線程已經(jīng)擁有了這個對象的鎖,就把鎖的計數(shù)器+1,其實本質(zhì)上就是通過這種方式實現(xiàn)了可重入性(而不是已擁有了鎖則不能繼續(xù)獲?。?。
5.面試官:說一說JVM底層對Java的原生鎖做了哪些優(yōu)化?
(1)畫外音:這有點難,特意上網(wǎng)參考了下,原來是關(guān)于鎖競爭和升級的……
(2)回答:在Java 6以前前,Monitor的實現(xiàn)完全依賴底層操作系統(tǒng)的互斥鎖來實現(xiàn),也就是上面在問題2中所闡述的獲取、釋放鎖的邏輯;由于Java的線程與操作系統(tǒng)的原生線程有映射關(guān)系,如果要將一個線程進行阻塞或喚起 都需要操作系統(tǒng)的協(xié)助,這就需要從用戶態(tài)切換到內(nèi)核態(tài)來執(zhí)行,這種切換代價十分昂貴,很耗處理器時間,現(xiàn)代JDK中做了大量的優(yōu)化;
一種優(yōu)化是使用自旋鎖,即在線程進行阻塞操作之前先讓線程自旋等待一段時間,可能在等待期間其他線程已經(jīng)解鎖,這時就無需再讓線程執(zhí)行阻塞操作,避免了用戶態(tài)到內(nèi)核態(tài)的切換。
而現(xiàn)代JDK中還提供了三種不同的Monitor實現(xiàn),也就是三種不同的鎖:偏向鎖、輕量級鎖、重量級鎖
這三種鎖使得JDK得以優(yōu)化Synchronized的運行,當(dāng)JVM檢測到不同的競爭狀況時,會自動切換到適合的鎖實現(xiàn),這就是鎖的升級、降級。
當(dāng)沒有競爭出現(xiàn)時,默認(rèn)使用偏向鎖,JVM會利用CAS操作,在對象頭上的MarkWord部分設(shè)置線程ID,以表示這個對象偏向于當(dāng)前線程,所以并不涉及真正的互斥鎖,因為在很多應(yīng)用場景中,大部分對象生命周期中最多會被一個線程鎖定,使用偏斜鎖可以降低無競爭開銷。
如果有另一線程試圖鎖定某個被偏向鎖鎖過的對象,JVM就會自動撤銷偏向鎖,切換到輕量級鎖實現(xiàn);輕量級鎖依賴CAS操作MarkWord來試圖獲取鎖,如果重試成功,就使用普通的輕量級鎖;否則,進一步升級為重量級鎖;
6.面試官:嗯,不錯,Synchronized是公平鎖還是非公平鎖,為什么?
(1)畫外音:這個倒不難……
(2)回答:非公平;非公平主要表現(xiàn)在獲取鎖的行為上:并非是按照申請鎖的時間前后給等待線程分配鎖的,每當(dāng)鎖被釋放后,任何一個線程都有機會競爭到鎖,這樣做的目的是為了提高執(zhí)行性能,缺點是可能會產(chǎn)生線程饑餓現(xiàn)象;
7.面試官: 為什么說Synchronized是悲觀鎖?
(1)畫外音:面試官的用意應(yīng)該是想讓候選人談?wù)剬Ρ^鎖的理解(下面的回答有點官方)
(2)回答:因為Synchronized的并發(fā)策略是悲觀的:即不管是否會產(chǎn)生競爭,任何的數(shù)據(jù)操作都必須要加鎖,包括“從用戶態(tài)切換到核心態(tài)”、“維護鎖計數(shù)器”和“檢查被阻塞的線程是否需要被喚醒”等操作;
隨著硬件指令集的發(fā)展,我們可以使用基于沖突檢測的樂觀并發(fā)策略,即先進行操作,如果沒有其他線程征用數(shù)據(jù),那操作就成功了;
如果共享數(shù)據(jù)有征用,產(chǎn)生了沖突,那就再進行其他的補償措施,這種樂觀的并發(fā)策略的許多實現(xiàn)不需要線程掛起,所以被稱為非阻塞同步。
8.面試官: 那你了解樂觀鎖嗎,它的實現(xiàn)原理又是什么,能講講嗎?
(1)畫外音:應(yīng)該是聊聊CAS機制……
(2)回答:樂觀鎖,顧名思義表示系統(tǒng)總是認(rèn)為當(dāng)前的并發(fā)情況是樂觀的,而不需要通過加各種鎖進行控制;
樂觀鎖的實現(xiàn)原理是CAS機制(Compare And Swap,比較并交換),一種在JUC中廣泛使用的算法;它涉及到三個操作數(shù):內(nèi)存值V、預(yù)期值A、新值B,當(dāng)且僅當(dāng)預(yù)期值A和內(nèi)存值V相等時才將內(nèi)存值V修改為新值B;
其底層實現(xiàn)邏輯:首先檢查某塊內(nèi)存的值是否跟之前我讀取的是一樣的,如果不一樣則表示期間此內(nèi)存值已經(jīng)被別的線程更改過,舍棄本次操作,否則說明期間沒有其他線程對此內(nèi)存值操作,可以把新值設(shè)置給此塊內(nèi)存,即間接意味著獲取鎖成功!
CAS具有原子性,它的原子性是由CPU硬件指令實現(xiàn)保證的,即通過JNI調(diào)用Native方法,從而調(diào)用由C++編寫的硬件級別指令,JDK中提供了Unsafe類來執(zhí)行這些操作(查看JUC很多類的底層源碼會發(fā)現(xiàn) Unsafe.compareAndSwapxxx() 的調(diào)用無處不在,很牛逼?。。。?span lang="EN-US">
9.面試官:那樂觀鎖就一定是好的嗎?
(1)畫外音:廢話,世間一切事物哪有什么是一定的,此處應(yīng)該是想讓候選人提到自旋消耗性能以及ABA的問題……
(2)回答:樂觀鎖可以避免 悲觀鎖獨占對象這一現(xiàn)象 的出現(xiàn),同時也提高了并發(fā)性能,但它也有一些缺點:
A. 樂觀鎖只能保證一個共享變量的原子操作:如果多一個或幾個變量,樂觀鎖將變得力不從心,但互斥鎖能輕易解決,不管對象數(shù)量多少及對象顆粒度大小;
B. 長時間自旋可能導(dǎo)致開銷大。假如CAS長時間不成功而一直自旋,會給CPU帶來很大的開銷;
C. ABA問題:CAS的核心思想是通過比對內(nèi)存值與預(yù)期值是否一樣而判斷內(nèi)存值是否被改過,但這個判斷邏輯不夠嚴(yán)謹(jǐn);
假如內(nèi)存值原來是A,后來被一線程改為B,最后又被改回了A,則CAS認(rèn)為此內(nèi)存值并沒有發(fā)生改變,但實際上是有被其他線程改過的,這種情況對依賴過程值的情景的運算結(jié)果影響很大。
10.面試官:剛提到ABA的問題,那有什么辦法解決嗎?
(1)畫外音:打個廣告~這個可以看看debug的“分布式鎖實戰(zhàn)視頻教程”,樂觀鎖那里就用的 version來實現(xiàn)的
(2)回答:解決的思路是引入版本號,每次變量更新時都把版本號加1,同時如果條件允許,還需要額外建立數(shù)據(jù)更新歷史表,并同時維護好版本號version 和 數(shù)據(jù)變更記錄的映射關(guān)系!
11.面試官:跟Synchronized相比,可重入鎖ReentrantLock的實現(xiàn)原理有什么不同?
(1)畫外音:打個廣告~這個可以看看debug的“分布式鎖實戰(zhàn)視頻教程”,樂觀鎖那里就用的 version來實現(xiàn)的
(2)回答:其實,幾乎所有鎖的實現(xiàn)原理都是為了達到同個目的:讓所有的線程都能看到某種標(biāo)記,同一時刻只能有一個線程獲取到鎖;
Synchronized通過在對象頭中設(shè)置標(biāo)記MarkWord實現(xiàn)了這一目的,是一種JVM原生的鎖實現(xiàn)方式;
而ReentrantLock以及所有的基于Lock接口的實現(xiàn)類,則是通過一個volitile關(guān)鍵字修飾的int類型變量,并保證每個線程都能擁有對該int變量的可見性和原子性,其本質(zhì)是基于所謂的AQS框架;
12.面試官:你剛剛提到了AQS,那你說說AQS的實現(xiàn)原理?
(1)畫外音:還真是不依不饒啊……
(2)回答:AQS,即 AbstractQueuedSynchronizer 抽象隊列同步器,是一個用來構(gòu)建鎖和同步器的類,JUC Lock包下的鎖(常用的有ReentrantLock、ReadWriteLock),以及其他的像Semaphore、CountDownLatch,甚至是早期的FutureTask等,都是基于AQS來構(gòu)建的;
A.AQS在內(nèi)部定義了一個變量:volatile int state,用于表示同步狀態(tài):當(dāng)線程調(diào)用lock方法時,如果state=0,說明沒有任何線程占有共享資源的鎖,可以獲得鎖并將state=1;如果state=1,則說明有線程目前正在使用共享變量,其他線程必須加入同步隊列進行等待。
B.AQS內(nèi)部是通過Node實體類來表示一個雙向鏈表結(jié)構(gòu)的同步隊列,完成線程獲取鎖的排隊工作,當(dāng)有線程獲取鎖失敗后,就被添加到隊列末尾。
Node類是對要訪問同步代碼的線程的封裝,包含了線程本身及其狀態(tài)waitStatus(它有五種不同的取值,分別表示是否被阻塞、是否等待喚醒、是否已經(jīng)被取消等),每個Node結(jié)點關(guān)聯(lián)其prev結(jié)點和next結(jié)點(指針),方便線程釋放鎖后快速喚醒下一個在等待的線程,是一個FIFO的過程;
Node類有兩個常量,SHARED和EXCLUSIVE,分別代表共享模式和獨占模式,所謂共享模式是一個鎖允許多條線程同時操作(信號量Semaphore就是基于AQS的共享模式實現(xiàn)的),獨占模式指的是同一個時間段只能有一個線程對共享資源進行操作,多余的請求線程需要排隊等待(如ReentranLock);
C.AQS通過內(nèi)部類Condition Object構(gòu)建等待隊列(可有多個),當(dāng)Condition調(diào)用wait()方法后,線程將會加入等待隊列中,而當(dāng)Condition調(diào)用signal()方法后,線程將從等待隊列轉(zhuǎn)移動同步隊列中競爭鎖。
D.AQS和Condition各自維護了不同的隊列,在使用Lock和Condition的時候,其實就是兩個隊列的互相移動。
13.面試官:請對比下Synchronized 和 ReentrantLock的異同?
(1)畫外音:筆者自行擴充的
(2)回答:ReentrantLock是Lock的實現(xiàn)類,是一個互斥的同步鎖;
A.從功能角度上看,ReentrantLock比Synchronized的同步操作更精細(xì)(因為可以像普通對象一樣使用),甚至實現(xiàn)了Synchronized沒有的高級功能,如:
等待可中斷:當(dāng)持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,對處理執(zhí)行時間非常長的同步塊很有用。
帶超時的獲取鎖嘗試:在指定的時間范圍內(nèi)獲取鎖,如果時間到了仍然無法獲取則返回,可以判斷是否有線程在排隊等待獲取鎖。
可以響應(yīng)中斷請求:與Synchronized不同,當(dāng)獲取到鎖的線程被中斷時,能夠響應(yīng)中斷,中斷異常將會被拋出,同時鎖會被釋放。
可以實現(xiàn)公平鎖:從鎖的釋放角度上看,Synchronized在JVM層面上實現(xiàn)的,不但可以通過一些監(jiān)控工具監(jiān)控Synchronized的鎖定,而且在代碼執(zhí)行出現(xiàn)異常時,JVM會自動釋放鎖定;但是使用Lock則不行,Lock是通過代碼實現(xiàn)的,要保證鎖定一定會被釋放,就必須將unLock()放到finally{}中。
B.從性能角度上看,Synchronized早期實現(xiàn)比較低效,對比ReentrantLock,大多數(shù)場景性能都相差較大,但是在Java6中對其進行了非常多的改進,在競爭不激烈時,Synchronized的性能要優(yōu)于ReetrantLock;在高競爭情況下,Synchronized的性能會下降幾十倍,但是ReetrantLock的性能能維持常態(tài)。
14.面試官:上面提到ReentrantLock也是一種可重入鎖,那它的底層又是如何實現(xiàn)的?
(1)畫外音:這……還是得聊聊AQS?(后面偷瞄了一會源碼)
(2)回答:ReentrantLock內(nèi)部自定義了同步器Sync(Sync既實現(xiàn)了AQS,又實現(xiàn)了AOS,而AOS提供了一種持有互斥鎖的方式),其實就是加鎖的時候通過CAS算法,將線程對象放到一個雙向鏈表中,每次獲取鎖的時候,看下當(dāng)前維護的那個線程ID和當(dāng)前請求的線程ID是否一樣,一樣就可重入了。
15.面試官:除了Synchronized 和 ReentrantLock,你還接觸過JUC下中的哪些并發(fā)工具?
(1)畫外音:總算人性化一點了,可以自圓其說了!
(2)回答:通常所說的并發(fā)包JUC其實就是java.util.concurrent包及其子包下集合了Java并發(fā)的各種基礎(chǔ)工具類,具體主要包括幾個方面:
A.提供了CountDownLatch、CyclicBarrier、Semaphore等,比Synchronized更加高級,可以實現(xiàn)更加豐富多線程操作的同步結(jié)構(gòu);
B.提供了ConcurrentHashMap、有序的ConcunrrentSkipListMap,或者通過類似快照機制實現(xiàn)線程安全的動態(tài)數(shù)組CopyOnWriteArrayList等,各種線程安全的容器;
C.提供了ArrayBlockingQueue、SynchorousQueue或針對特定場景的PriorityBlockingQueue等,各種并發(fā)隊列的實現(xiàn);
D.強大的Executor框架,可以創(chuàng)建各種不同類型的線程池,調(diào)度任務(wù)運行等。
16.面試官:簡單說一說ReadWriteLock 和StampedLock 吧?
(1)畫外音:問得還比較多……
(2)回答:雖然ReentrantLock和Synchronized簡單實用,但是行為上有一定的局限性,要么不占,要么獨占;在實際應(yīng)用場景中,有時候不需要大量競爭的寫操作,而是以并發(fā)讀為主,為了進一步優(yōu)化并發(fā)操作的粒度,Java提供了讀寫鎖;
讀寫鎖基于的原理是:多個讀操作不需要互斥,如果讀鎖試圖鎖定時,寫鎖卻被某個線程持有時,讀鎖將無法獲得,而只好等待對方操作結(jié)束,這樣就可以自動保證不會讀取到臟數(shù)據(jù);
ReadWriteLock代表了一對鎖,它在數(shù)據(jù)量大 且 并發(fā)讀多、寫少的時候,能夠比純同步版本凸顯出優(yōu)勢;
讀寫鎖看起來比Synchronized的粒度似乎細(xì)一些,但在實際應(yīng)用中,其表現(xiàn)也并不盡人意,主要還是因為相對比較大的開銷;
所以,JDK在后期引入了StampedLock,在提供類似讀寫鎖的同時,還支持優(yōu)化讀模式,優(yōu)化讀是基于這樣的假設(shè):大多數(shù)情況下讀操作并不會和寫操作沖突,其邏輯是先試著修改,然后通過validate方法確認(rèn)是否進入了寫模式,如果沒有進入,就成功避免了開銷;如果進入,則嘗試獲取讀鎖。
17.面試官:如何讓Java的線程彼此同步?你了解過哪些同步器?
(1)畫外音:應(yīng)該是想聊JUC 同步器的三個成員 : CountDownLatch、 CyclicBarrier
和 Semaphore,我不玩了……
面試官:看出了我的不悅,詭異一笑,好吧,關(guān)于Java的鎖和同步咱們聊得也比較多了,咱們換個話題吧!??!
下一回合,咱們聊聊 Java的線程池?。?!
總結(jié):