由 并發(fā)編程中常見的鎖策略 總結(jié)可知,synchronized 具有以下幾個(gè)特性:
- 開始時(shí)是樂觀鎖,如果鎖沖突頻繁,就轉(zhuǎn)換為悲觀鎖。
- 開始是輕量級鎖實(shí)現(xiàn),如果鎖被持有的時(shí)間較長,就轉(zhuǎn)換成重量級鎖。
- 實(shí)現(xiàn)輕量級鎖時(shí),大概率用自旋鎖策略。
- 是一種不公平鎖。
- 是一種可重入鎖。
- 不是讀寫鎖。
本文介紹synchronized的幾種優(yōu)化操作,包括鎖升級、鎖消除和鎖粗化。
一、鎖升級
JVM 將 synchronized 鎖分為無鎖、偏向鎖、輕量級鎖、重量級鎖這四種狀態(tài)。在加鎖過程中,會根據(jù)實(shí)際情況,依次進(jìn)行升級。(**目前主流的 JVM 的實(shí)現(xiàn),只能鎖升級,不能鎖降級!**不是無法實(shí)現(xiàn),只不過可能是因?yàn)榇嬖谝恍┐鷥r(jià),使得這樣做的收益和代價(jià)不成比例,因此就沒有實(shí)現(xiàn)。)
整體的加鎖過程(鎖升級過程):剛開始加鎖,是偏向鎖狀態(tài);遇到鎖競爭后,升級成自旋鎖(輕量級鎖);當(dāng)競爭更激烈時(shí),就會變成重量級鎖(交給內(nèi)核阻塞等待)。
1、偏向鎖(Biased Locking)
第一個(gè)嘗試加鎖的線程優(yōu)先進(jìn)入偏向鎖狀態(tài)。偏向鎖是Java虛擬機(jī)(JVM)中用于提高線程同步性能的一種優(yōu)化技術(shù)。在多線程環(huán)境中,對共享資源進(jìn)行同步操作,需要使用鎖(synchronized)來保證線程的互斥訪問。傳統(tǒng)的鎖機(jī)制存在競爭和上下文切換的開銷,對性能會有一定的影響。而偏向鎖則是為了減少無競爭情況下的鎖操作開銷而引入的。
偏向鎖不是真的“加鎖”,只是先讓線程針對鎖對象有個(gè)標(biāo)記,記錄某個(gè)鎖屬于哪個(gè)線程。
它的基本思想是,當(dāng)一個(gè)線程獲取鎖并訪問同步代碼塊時(shí),如果沒有競爭,那么下次該線程再次進(jìn)入同步塊時(shí),無需再次獲取鎖。這是因?yàn)樵跓o競爭的情況下,假設(shè)一個(gè)線程反復(fù)訪問同步代碼塊,無需每次都去競爭鎖,只需判斷鎖是否處于偏向狀態(tài);如果是,那么直接進(jìn)入同步代碼塊即可。
通俗來說就是,如果后續(xù)沒有其他線程再來競爭該鎖,那么就不用真的加鎖了,從而避免了加鎖解鎖的開銷。 但一旦還有其他線程來嘗試競爭這個(gè)鎖,偏向鎖就立即升級成真的鎖(輕量級鎖),此時(shí)別的線程就只能等待了。這樣做既保證了效率,也保證了線程安全。
如何判定有沒有別的線程來競爭該鎖?
注意,偏向鎖是synchronized內(nèi)部做的工作。synchronized會針對某個(gè)對象進(jìn)行加鎖,這個(gè)所謂的“偏向鎖”正是在這個(gè)對象里頭做一個(gè)標(biāo)記。
由于一開始已經(jīng)在鎖對象中記錄了當(dāng)前鎖屬于哪個(gè)線程,因此很容易識別當(dāng)前申請鎖的線程是否是一開始就記錄了的線程。
如果另一個(gè)線程正在嘗試對同一個(gè)對象進(jìn)行加鎖,也會先嘗試做標(biāo)記,但結(jié)果卻發(fā)現(xiàn)已經(jīng)有標(biāo)記了。于是JVM就會通知先來的線程,讓它趕快把鎖升級一下。
偏向鎖本質(zhì)上是“延遲加鎖”,即能不加鎖就不加鎖,盡量避免不必要的加鎖開銷;但是該做的標(biāo)記還是得做的,否則就無法區(qū)分何時(shí)需要真正加鎖。
舉個(gè)栗子理解偏向鎖
假設(shè)男主是一個(gè)鎖,女主是一個(gè)線程。如果只有女主和男主曖昧(即只有這一個(gè)線程來使用這個(gè)鎖),那么即使男主和女主不領(lǐng)證結(jié)婚(避免了高成本操作),也可以一直生活下去。
但是如果此時(shí)有女配出現(xiàn),也嘗試競爭男主,想和男主搞曖昧,那么此時(shí)女主就必須當(dāng)機(jī)立斷,不管領(lǐng)證結(jié)婚這個(gè)操作成本多高,也勢必要把這個(gè)動作完成(即真正加鎖),讓女配死心。
所以說,偏向鎖 = 搞曖昧~~
2、自旋鎖
**什么是自旋鎖?**在鎖策略的文章中提到:
自旋鎖是一種典型的輕量級鎖的實(shí)現(xiàn)方式,它通常是純用戶態(tài)的,不需要經(jīng)過內(nèi)核態(tài)。按之前的方式,線程在搶鎖失敗后即進(jìn)入阻塞狀態(tài),放棄 CPU,需要過很久才能再次被調(diào)度。但實(shí)際上,在大部分情況下雖然當(dāng)前搶鎖失敗,但過不了很久鎖就會被釋放,沒必要就放棄 CPU。這個(gè)時(shí)候就可以使用自旋鎖來處理這樣的問題。
自旋鎖是一種忙等待鎖的機(jī)制。當(dāng)一個(gè)線程需要獲取自旋鎖時(shí),它會反復(fù)地檢查鎖是否可用,而不是立即被阻塞。如果獲取鎖失?。ㄦi已經(jīng)被其他線程占用),當(dāng)前線程會立即再嘗試獲取鎖,不斷自旋(空轉(zhuǎn))等待鎖的釋放,直到獲取到鎖為止。第一次獲取鎖失敗,第二次的嘗試會在極短的時(shí)間內(nèi)到來。這樣能保證一旦鎖被其他線程釋放,當(dāng)前線程能第一時(shí)間獲取到鎖。
優(yōu)點(diǎn):沒有放棄 CPU,不涉及線程阻塞和調(diào)度。一旦鎖被釋放就能第一時(shí)間獲取到鎖。
缺點(diǎn):如果鎖被其他線程持有的時(shí)間比較久,那么就會持續(xù)的消耗 CPU 資源(忙等),而掛起等待的時(shí)候是不消耗 CPU 的。自旋鎖適用于保護(hù)臨界區(qū)較小、鎖占用時(shí)間短的情況,因?yàn)樽孕龝腃PU資源。自旋鎖通常使用原子操作或特殊的硬件指令來實(shí)現(xiàn)。
隨著其他線程進(jìn)入鎖競爭,偏向鎖狀態(tài)會被消除,進(jìn)入輕量級鎖狀態(tài),即自適應(yīng)的自旋鎖。
此處的輕量級鎖是通過 CAS 來實(shí)現(xiàn)。通過 CAS 檢查并更新一塊內(nèi)存 (比如比較 null 與該線程引用是否相等),如果更新成功,則認(rèn)為加鎖成功;如果更新失敗,則認(rèn)為鎖被占用,繼續(xù)自旋式的等待,期間并不放棄 CPU 資源。
(見 詳解CAS算法)
由于自旋操作是一直讓 CPU 空轉(zhuǎn),比較浪費(fèi) CPU 資源,因此此處的自旋不會一直持續(xù)進(jìn)行,而是達(dá)到一定的時(shí)間或重試次數(shù)就不再自旋了。這也就是所謂的 “自適應(yīng)”。
3、重量級鎖
**什么是重量級鎖 ?**在鎖策略的文章中提到:
簡單來說,輕量級鎖是加鎖解鎖的過程更快更高效的鎖策略,而重量級鎖是加鎖解鎖的過程更慢更低效的鎖策略。重量級鎖中加鎖機(jī)制重度依賴 OS 提供的 mutex(互斥量)。
- 大量的內(nèi)核態(tài)用戶態(tài)切換。
- 很容易引發(fā)線程的調(diào)度。
這兩個(gè)操作的成本都比較高,而且一旦涉及到用戶態(tài)和內(nèi)核態(tài)的切換,效率就低了。
如果競爭進(jìn)一步激烈,自旋不能快速獲取到鎖狀態(tài)。就會膨脹為重量級鎖。
自旋鎖雖然能最快獲取到鎖,但是要消耗大量 CPU(因?yàn)樽孕臅r(shí)候CPU是快速空轉(zhuǎn)的)。如果當(dāng)前鎖競爭非常激烈,比如 50 個(gè)線程競爭一個(gè)鎖,1 個(gè)爭上,另外 49 個(gè)等待。這么多線程都在自旋空轉(zhuǎn),CPU的消耗就非常大。既然如此,就更改鎖策略,升級成重量級鎖,讓其它的線程都在內(nèi)核里進(jìn)行阻塞等待(這意味著線程要暫時(shí)放棄 CPU 資源,由內(nèi)核進(jìn)行后續(xù)調(diào)度)。
(PS:目前的主流操作系統(tǒng)如 windows,Linux,調(diào)度的開銷都是很大的。系統(tǒng)不承諾能在 xx 時(shí)間內(nèi)一定能完成指定的調(diào)度,極端情況下調(diào)度的開銷可能非常大。
但還存在另外一種實(shí)時(shí)操作系統(tǒng)(例如 vxworks),它能夠以更低的成本完成任務(wù)調(diào)度,但犧牲了更多的其他功能。在如火箭發(fā)射這種對時(shí)間精度比較高的特殊領(lǐng)域就會用到。)
如果競爭進(jìn)一步激烈,自旋不能快速獲取到鎖狀態(tài)。就會膨脹為重量級鎖。
此處的重量級鎖就是指內(nèi)核提供的 mutex 。
- 某線程執(zhí)行加鎖操作,先進(jìn)入內(nèi)核態(tài)。
- 在內(nèi)核態(tài)判定當(dāng)前鎖是否已經(jīng)被別的線程占用 。
- 如果該鎖沒有占用,則加鎖成功,并切換回用戶態(tài)。
- 如果該鎖被占用,則加鎖失敗。此時(shí)線程進(jìn)入鎖的等待隊(duì)列并掛起,等待被操作系統(tǒng)喚醒。
- 經(jīng)歷了一系列的“滄海桑田”,這個(gè)鎖終于被其他線程釋放了,此時(shí)操作系統(tǒng)也想起了這個(gè)被掛起的線程,于是喚醒這個(gè)線程,并讓它嘗試重新獲取鎖。
二、鎖消除
鎖消除也是“非必要,不加鎖”的一種體現(xiàn)。與鎖升級不同,鎖升級是程序在運(yùn)行階段 JVM 做出的優(yōu)化手段。而鎖消除是在程序編譯階段的優(yōu)化手段。編譯器和 JVM 會檢測當(dāng)前代碼是否是多線程執(zhí)行或是否有必要加鎖。如果無必要,但又把鎖給寫了,那么在編譯的過程中就會自動把鎖去掉。
有些應(yīng)用程序代碼中可能會用到?jīng)]有必要用到的 synchronized。例如 StringBuffer 就是線程安全的,它的每一個(gè)關(guān)鍵方法都加了synchronized關(guān)鍵字:
但這里就有一個(gè)問題:如果是在單線程中使用StringBuffer,是不涉及線程安全問題的。這個(gè)時(shí)候其實(shí)就沒必要加鎖。那么這時(shí)編譯器就會出手,發(fā)現(xiàn)synchronized是沒必要加的,就會在編譯階段把synchronized去掉,相當(dāng)于加鎖操作沒有真正被編譯。
StringBuffer sb = new StringBuffer(); sb.append("a"); sb.append("b"); sb.append("c"); sb.append("d");
此時(shí),每個(gè) append 的調(diào)用都會涉及加鎖和解鎖。但如果只是在單線程中執(zhí)行這段代碼,那么其中的這些加鎖解鎖操作是沒有必要的,白白浪費(fèi)了一些資源開銷。
鎖消除整體來說是一個(gè)比較保守的優(yōu)化手段,畢竟編譯器肯定得保證消除的操作是靠譜的。所以只有十拿九穩(wěn)的時(shí)候才會實(shí)施鎖消除,否則仍然會上鎖,這時(shí)就會交給其它的操作策略來對鎖進(jìn)行優(yōu)化(比如上面的鎖升級)。
三、鎖粗化
鎖的粒度指的是 synchronized 代碼塊中包含代碼的多少。代碼越多,粒度越大;代碼越少,粒度越小。
一般我們在寫代碼時(shí),多數(shù)情況下是希望鎖的粒度更小一點(diǎn)。(鎖的粒度小就意味著串行執(zhí)行的代碼更少,并發(fā)執(zhí)行的代碼更多)。如果某個(gè)場景需要頻繁地加鎖解鎖,此時(shí)編譯器就可能把這個(gè)操作優(yōu)化成個(gè)粒度更粗的鎖,即鎖的粗化。
實(shí)際開發(fā)過程中使用細(xì)粒度鎖,是期望釋放鎖的時(shí)候其他線程能使用鎖。但是實(shí)際中可能并沒有其他線程來搶占這個(gè)鎖。這種情況 JVM 就會自動把鎖粗化,避免頻繁申請釋放鎖造成不必要的開銷。
舉個(gè)栗子理解鎖粗化
上班時(shí)要向領(lǐng)導(dǎo)匯報(bào)工作。你的領(lǐng)導(dǎo)給你安排了三個(gè)工作:A、B、C。
匯報(bào)方式有:
- 先打個(gè)電話,匯報(bào)工作 A 的進(jìn)展,掛了電話;再打個(gè)電話,匯報(bào)工作 B 的進(jìn)展,掛了電話;再打個(gè)電話,匯報(bào)工作C的進(jìn)展,掛了電話。(你給領(lǐng)導(dǎo)打電話,領(lǐng)導(dǎo)接你的電話,領(lǐng)導(dǎo)就干不了別的;別人要給領(lǐng)導(dǎo)打電話,就只能阻塞等待。每次鎖競爭都可能引入一定的等待開銷,此時(shí)整體的效率可能反而更低。)
- 打個(gè)電話,一口氣匯報(bào) 工作 A,工作B,工作 C,掛了電話。
顯然第二種方式是更加高效的。文章來源:http://www.zghlxwxcb.cn/news/detail-596739.html
可見,synchronized 的策略是比較復(fù)雜的,它是一個(gè)很“智能”的鎖。文章來源地址http://www.zghlxwxcb.cn/news/detail-596739.html
到了這里,關(guān)于Java 中 synchronized 的優(yōu)化操作:鎖升級、鎖消除、鎖粗化的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!