java 等待几秒_Java并发编程synchronized相关面试题总结
說說自己對于synchronized關(guān)鍵字的了解
synchronized關(guān)鍵字用于解決多個線程之間訪問資源的同步性,synchronized關(guān)鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執(zhí)行。
值得注意的是,在Java早期,JDK1.6之前,synchronized屬于重量級鎖,效率低下。
原因在于:
監(jiān)視器鎖【monitor】依賴于底層操作系統(tǒng)的Mutex Lock實(shí)現(xiàn),Java的線程是映射到操作系統(tǒng)的原生線程之上的。如果要掛起或喚醒一個線程,都需要操作系統(tǒng)幫忙完成,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)化到內(nèi)核態(tài),需要消耗比較長的時間。
但是,JDK1.6之后,Java官方從JVM層面對synchronized關(guān)鍵字進(jìn)行了較大的優(yōu)化,效率不可同日而語。主要的優(yōu)化有:自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術(shù)來減少鎖操作的開銷。
synchronized關(guān)鍵字的三種使用
修飾實(shí)例方法:作用于當(dāng)前對象實(shí)例加鎖,進(jìn)入同步代碼前要獲得?當(dāng)前對象實(shí)例的鎖。
修飾靜態(tài)方法:?也就是給當(dāng)前類加鎖,會作用于類的所有對象實(shí)例 ,進(jìn)入同步代碼前要獲得?當(dāng)前 class 的鎖。
注意:靜態(tài)成員不屬于任何一個實(shí)例對象,是類成員!因此,一個線程A調(diào)用一個實(shí)例對象的非靜態(tài)synchronized方法,一個線程B調(diào)用這個實(shí)例對象的所屬類的靜態(tài)synchronized方法,是被允許的。?因為訪問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的鎖,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對象鎖。
修飾代碼塊?:給括號內(nèi)配置的對象加鎖。synchronized(this|object) 表示進(jìn)入同步代碼庫前要獲得給定對象的鎖。synchronized(類.class) 表示進(jìn)入同步代碼前要獲得?當(dāng)前 class 的鎖。
synchronized關(guān)鍵字的底層原理
通過對.class文件的編譯可以發(fā)現(xiàn):
同步方法通過ACC_SYNCHRONIZED修飾。
代碼塊同步使用monitorenter和monitorexit兩個指令實(shí)現(xiàn)。
雖然兩者實(shí)現(xiàn)細(xì)節(jié)不同,但其實(shí)本質(zhì)上都是JVM基于進(jìn)入和退出Monitor對象來實(shí)現(xiàn)同步,JVM的要求如下:
monitorenter指令會在編譯后插入到同步代碼塊的開始位置,而monitorexit則會插入到方法結(jié)束和異常處。
每個對象都有一個monitor與之關(guān)聯(lián),且當(dāng)一個monitor被持有之后,他會處于鎖定狀態(tài)。
線程執(zhí)行到monitorenter時,會嘗試獲取對象對應(yīng)monitor的所有權(quán)。
在獲取鎖時,如果對象沒被鎖定,或者當(dāng)前線程已經(jīng)擁有了該對象的鎖(可重進(jìn)入,不會鎖死自己),將鎖計數(shù)器加一,執(zhí)行monitorexit時,鎖計數(shù)器減一,計數(shù)為零則鎖釋放。
獲取對象鎖失敗,則當(dāng)前線程陷入阻塞,直到對象鎖被另外一個線程釋放。
JDK1.6之后對synchronized關(guān)鍵字進(jìn)行的優(yōu)化
優(yōu)化:偏向鎖,輕量級鎖,自旋鎖,適應(yīng)性自旋鎖,鎖消除,鎖粗化。
鎖主要存在的四種狀態(tài),依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)、重量級鎖狀態(tài),他們會隨著競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。
Java對象頭的組成
鎖存在于Java對象頭里,對象頭的組成部分:
Mark Word:存儲對象的hashCode或鎖信息等。
Class Metadata Address:存儲到對象類型數(shù)據(jù)的指針。
Array length:數(shù)組的長度(如果當(dāng)前對象是數(shù)組)
Java對象頭又存在于Java堆中,堆內(nèi)存分為三部分:對象頭,實(shí)例數(shù)據(jù)和對齊填充。
MarkWord的組成
Java對象頭的MardWord中記錄了對象和鎖的相關(guān)信息,無鎖狀態(tài)下,Java對象頭里的Mark Word里默認(rèn)存儲對象的HashCode、分代年齡和鎖標(biāo)記位。在64位的JVM中,Mark Word為64 bit。
在運(yùn)行期間Mark Word里存儲的數(shù)據(jù)會隨著鎖標(biāo)志位的變化而變化。鎖升級的功能也主要靠MarkWord中鎖標(biāo)志位和是否偏向鎖標(biāo)志完成。
鎖升級的過程
鎖升級的過程:無鎖,偏向鎖,輕量級鎖,重量級鎖
偏向鎖
HotSpot的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。
偏向鎖的適用場景
偏向鎖主要用于優(yōu)化:同一線程多次申請同一個鎖的競爭,在某些情況下,大部分時間都是同一個線程競爭鎖資源的。
偏向鎖的加鎖
主要流程:當(dāng)一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時不需要進(jìn)行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖。
如果測試成功,表示線程已經(jīng)獲得了鎖。
如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置,則使用CAS競爭鎖。如果設(shè)置了,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程。
偏向鎖的撤銷
一旦出現(xiàn)其他線程競爭鎖資源時,偏向鎖就會被撤銷。偏向鎖的撤銷可能需要等待全局安全點(diǎn)【在這個時間點(diǎn)上沒有正在執(zhí)行的字節(jié)碼】。
首先暫停持有該鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動狀態(tài),則將對象頭設(shè)置成無鎖狀態(tài)。
如果持有偏向鎖的線程仍然活著,擁有偏向鎖的棧會被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程,要么恢復(fù)到無鎖或者標(biāo)記對象不適合作為偏向鎖,最后喚醒在暫停的線程。
偏向鎖的關(guān)閉
偏向鎖在Java 6和Java 7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0。
如果說通常處于競爭狀態(tài),可以通過- XX:-UseBiasedLocking=false,進(jìn)入輕量級鎖狀態(tài)。
輕量級鎖
如偏向鎖存在,如有另一線程競爭鎖,且對象頭MarkWord中的線程ID與當(dāng)前線程ID不同,則該線程將會嘗試CAS操作獲取鎖,獲取失敗,代表鎖存在競爭,偏向鎖向輕量級鎖升級。
輕量級鎖的加鎖
線程在執(zhí)行同步塊之前,JVM會先在當(dāng)前線程的棧臺中創(chuàng)建用于存儲鎖記錄的空間【Displaced Mark Word】,并將對象頭中的Mark Word復(fù)制到鎖記錄中。
然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。替換成功,則當(dāng)前線程獲得鎖。替換失敗,表示其他線程競爭鎖,當(dāng)前線程嘗試使用自旋來獲取鎖。
輕量級鎖的解鎖
使用原子的CAS操作將【Displaced Mark Word】替換回對象頭。替換成功,表示沒有競爭發(fā)生。替換失敗,表示當(dāng)前鎖存在競爭,鎖就會膨脹成重量級鎖。
輕量級鎖的適用場景
線程交替執(zhí)行同步塊,絕大部分的鎖在整個同步周期內(nèi)都不存在長時間的競爭。
鎖的優(yōu)缺點(diǎn)對比
鎖優(yōu)點(diǎn)缺點(diǎn)適用場景偏向鎖加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法比僅存在納秒級的差距。如果線程間存在鎖競爭, 會帶來額外的鎖撤銷的消耗。適用于只有一個線程訪問同步塊場景。輕量級鎖競爭的線程不會阻塞,提高了程序的響應(yīng)速度。如果始終得不到所競爭的線程使用自旋會消耗CPU。追求響應(yīng)時間。同步塊執(zhí)行速度非常快。重量級鎖線程競爭不使用自旋,不會消耗CPU。線程阻塞,響應(yīng)時間緩慢。追求吞吐量。同步塊執(zhí)行速度較長。
總結(jié)
JVM在JDK 1.6中引入了分級鎖機(jī)制來優(yōu)化synchronized
當(dāng)一個線程獲取鎖時,首先對象鎖成為一個偏向鎖這是為了避免在同一線程重復(fù)獲取同一把鎖時,用戶態(tài)和內(nèi)核態(tài)頻繁切換
如果有多個線程競爭鎖資源,鎖將會升級為輕量級鎖這適用于在短時間內(nèi)持有鎖,且分鎖交替切換的場景輕量級鎖還結(jié)合了自旋鎖來避免線程用戶態(tài)與內(nèi)核態(tài)的頻繁切換
如果鎖競爭太激烈(自旋鎖失敗),同步鎖會升級為重量級鎖
優(yōu)化synchronized同步鎖的關(guān)鍵:減少鎖競爭應(yīng)該盡量使synchronized同步鎖處于輕量級鎖或偏向鎖,這樣才能提高synchronized同步鎖的性能常用手段減少鎖粒度:降低鎖競爭減少鎖的持有時間,提高synchronized同步鎖在自旋時獲取鎖資源的成功率,避免升級為重量級鎖
在鎖競爭激烈時,可以考慮禁用偏向鎖和禁用自旋鎖
synchronized關(guān)鍵字與ReentrantLock的區(qū)別
共同點(diǎn)
都是可重入鎖:自己可以再次獲取自己的內(nèi)部鎖【避免一個線程獲取鎖之后,再次嘗試獲取鎖時造成的死鎖】。同一線程每次獲取鎖,計數(shù)器加一,釋放鎖,計數(shù)器減一,計數(shù)為0,代表完全釋放該鎖。
不同點(diǎn)
synchronized依賴于JVM實(shí)現(xiàn),ReentrantLock依賴于API。
相比synchronized,ReentrantLock增加了一些高級功能。主要來說主要有三點(diǎn):等待可中斷?: ReentrantLock提供了一種能夠中斷等待鎖的線程的機(jī)制,通過 lock.lockInterruptibly() 來實(shí)現(xiàn)這個機(jī)制。也就是說正在等待的線程可以選擇放棄等待,改為處理其他事情。可實(shí)現(xiàn)公平鎖?: ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。ReentrantLock默認(rèn)情況是非公平的,可以通過 ReentrantLock類的ReentrantLock(boolean fair)構(gòu)造方法來制定是否是公平的。可實(shí)現(xiàn)選擇性通知(鎖可以綁定多個條件): synchronized關(guān)鍵字與wait()和notify()/notifyAll()方法相結(jié)合可以實(shí)現(xiàn)等待/通知機(jī)制。ReentrantLock類當(dāng)然也可以實(shí)現(xiàn),但是需要借助于Condition接口與newCondition()方法。
如果感覺本文對你有幫助,點(diǎn)贊再看支持一下
總結(jié)
以上是生活随笔為你收集整理的java 等待几秒_Java并发编程synchronized相关面试题总结的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python写订单管理系统_利用Pyth
- 下一篇: springaop事务逻辑原理_架构师: