synchronized 實(shí)現(xiàn)原理與內(nèi)存屏障

鎖概述

我們知道線程安全問題的產(chǎn)生前提是多個(gè)線程并發(fā)訪問共享變量、共享資源(以下統(tǒng)稱為共享數(shù)據(jù))。于是,我們很容易想到保障線程安全的方法將多個(gè)線程對(duì)共享數(shù)據(jù)的并發(fā)訪問轉(zhuǎn)換為串行訪問,即一個(gè)共享數(shù)據(jù)一次只能被一個(gè)線程訪問,該線程訪問結(jié)束后其他線程才能對(duì)其進(jìn)行訪問。鎖(Lock)就是利用這種思路以保障線程安全的線程同步機(jī)制。

按照上述思路,鎖可以理解為對(duì)共享數(shù)據(jù)進(jìn)行保護(hù)的許可證。對(duì)于同一個(gè)許可證所保護(hù)的共享數(shù)據(jù)而言,任何線程訪問這些共享數(shù)據(jù)前必須先持有該許可證。一個(gè)線程只有在持有許可證的情況下才能夠?qū)@些共享數(shù)據(jù)進(jìn)行訪問;并且,一個(gè)許可證一次只能夠被一個(gè)線程持有;許可證的持有線程在其結(jié)束對(duì)這些共享數(shù)據(jù)的訪問后必須讓出(釋放)其持有的許可證,以便其他線程能夠?qū)@些共享數(shù)據(jù)進(jìn)行訪問。

一個(gè)線程在訪問共享數(shù)據(jù)前必須申請相應(yīng)的鎖(許可證),線程的這個(gè)動(dòng)作被稱為鎖的獲得(Acquire)。一個(gè)線程獲得某個(gè)鎖(?持有許可證?),我們就稱該線程為相應(yīng)鎖的持有線程?(?線程持有許可證?),一個(gè)鎖一次只能被一個(gè)線程持有。鎖的持有線程可以對(duì)該鎖所保護(hù)的共享數(shù)據(jù)進(jìn)行訪問,訪問結(jié)束后該線程必須釋放?(?Release?)?相應(yīng)的鎖。鎖的持有線程在其獲得鎖之后和釋放鎖之前這段時(shí)間內(nèi)所執(zhí)行的代碼被稱為臨界區(qū)(?Critical?Section?)。因此,共享數(shù)據(jù)只允許在臨界區(qū)內(nèi)進(jìn)行訪問,臨界區(qū)一次只能被一個(gè)線程執(zhí)行。

鎖具有排他性(Exclusive),即一個(gè)鎖一次只能被一個(gè)線程持有。因此,這種鎖被稱為排他鎖或者互斥鎖?(?Mutex?)。這種鎖的實(shí)現(xiàn)方式代表了鎖的基本原理,如圖所示。?

按照J(rèn)ava虛擬機(jī)對(duì)鎖的實(shí)現(xiàn)方式劃分,Java?平臺(tái)中的鎖包括內(nèi)部鎖(?Intrinsic??Lock?)和顯式鎖?(?Explicit?Lock?)。內(nèi)部鎖是通過synchronized關(guān)鍵字實(shí)現(xiàn)的;顯式鎖是通過java.concurrent.locks.Lock接口的實(shí)現(xiàn)類(如?java.concurrent.locks.ReentrantLock?類?)?實(shí)現(xiàn)的。

鎖的作用

鎖能夠保護(hù)共享數(shù)據(jù)以實(shí)現(xiàn)線程安全,其作用包括保障原子性、保障可見性和保障有序性。

鎖是通過互斥保障原子性的。所謂互斥 ( Mutual Exclusion ),就是指一個(gè)鎖一次只能被一個(gè)線程持有。因此一個(gè)線程持有一個(gè)鎖的時(shí)候,其他線程無法獲得該鎖,而只能等待其釋放該鎖后再申請。這就保證了臨界區(qū)代碼一次只能夠被一個(gè)線程執(zhí)行。因此,一個(gè)線程執(zhí)行臨界區(qū)期間沒有其他線程能夠訪問相應(yīng)的共享數(shù)據(jù),這使得臨界區(qū)代碼所執(zhí)行的操作自然而然地具有不可分割的特性,即具備了原子性。

從互斥的角度來看,鎖其實(shí)是將多個(gè)線程對(duì)共享數(shù)據(jù)的訪問由本來的并發(fā)( 未使用鎖的情況下 )改為串行( 使用鎖之后 )。因此,雖然實(shí)現(xiàn)并發(fā)是多線程編程的目標(biāo),但是這種并發(fā)往往是并發(fā)中帶有串行的局部并發(fā)。這好比公路維修使得多股車道在某處被合并成一股小車道,從而使原本在多股車道上并駕齊驅(qū)的車輛不得不“魚貫而行”。

我們知道,可見性的保障是通過寫線程沖刷處理器緩存和讀線程刷新處理器緩存這兩個(gè)動(dòng)作實(shí)現(xiàn)的。在Java平臺(tái)中,鎖的獲得隱含著刷新處理器緩存這個(gè)動(dòng)作,這使得讀線程在執(zhí)行臨界區(qū)代碼前( 獲得鎖之后 ) 可以將寫線程對(duì)共享變量所做的更新同步到該線程執(zhí)行處理器的高速緩存中;而鎖的釋放隱含著沖刷處理器緩存這個(gè)動(dòng)作,這使得寫線程對(duì)共享變量所做的更新能夠被“推送” 到該線程執(zhí)行處理器的高速緩存中,從而對(duì)讀線程可同步。因此,鎖能夠保障可見性。

鎖能夠保障有序性。寫線程在臨界區(qū)中所執(zhí)行的一系列操作在讀線程所執(zhí)行的臨界區(qū)看起來像是完全按照源代碼順序執(zhí)行,即讀線程對(duì)這些操作的感知順序與源代碼順序一致。這是暫且對(duì)原子性和可見性的保障的結(jié)果。設(shè)寫線程在臨界區(qū)中更新了b 、c 和 flag 這 3 個(gè)共享變量,如下代碼片段所示 :

由于鎖對(duì)可見性的保障,寫線程在臨界區(qū)中對(duì)上述任何一個(gè)共享變量所做的更新都對(duì)讀線程可見。并且,由于臨界區(qū)內(nèi)的操作具有原子性,因此寫線程對(duì)上述共事變量的更新會(huì)同時(shí)對(duì)讀線程可見,即在讀線程看來這些變量就像是在同一刻被更新的。因此讀線程并無法(也沒有必要)區(qū)分寫線程實(shí)際上是以什么順序更新上述變量的,這意味著讀線程可以認(rèn)為寫線程是依照源代碼順序更新上述共享變量的,即有序性得以保障。

盡管鎖能夠保障有序性,但是這并不意味著臨界區(qū)內(nèi)的內(nèi)存操作不能夠被重排序。臨界區(qū)內(nèi)的任意兩個(gè)操作依然可以在臨界區(qū)之內(nèi)被重排序(即不會(huì)重排到臨界區(qū)之外)。由于臨界區(qū)內(nèi)的操作具有的原子性,寫線程在臨界區(qū)內(nèi)對(duì)各個(gè)共享數(shù)據(jù)的更新同時(shí)對(duì)讀線程可見,因此這種重排序并不會(huì)對(duì)其他線程產(chǎn)生影響。

在理解,以及使用鎖保證線程安全的時(shí)候,需要注意鎖對(duì)可見性、原子性和有序性的保障是有條件的,我們要同時(shí)保證以下兩點(diǎn)得以滿足。

? 這些線程在訪問同一組共享數(shù)據(jù)的時(shí)候必須使用同一個(gè)鎖。

? 這些線程中的任意一個(gè)線程,即使其僅僅是讀取這組共享數(shù)據(jù)而沒有對(duì)其進(jìn)行更新的話,也需要在讀取時(shí)持有相應(yīng)的鎖。

上述任意一個(gè)條件未滿足都會(huì)使原子性、可見性和有序性沒有保障??梢姡覀冋f鎖能夠保護(hù)共享數(shù)據(jù)其實(shí)是一種“協(xié)議”?的結(jié)果,這個(gè)協(xié)議就是任何訪問該共享數(shù)據(jù)的寫線程、?讀線程都要滿足上述條件。只要有任何一個(gè)線程沒有遵守這個(gè)協(xié)議實(shí)際上就被打破,從而無法保障線程安全。這就好比交通規(guī)則(?“協(xié)議”?)?要靠人人都遵守才能保障交通安全一樣。

Java平臺(tái)中的任何一個(gè)對(duì)象都有唯一一個(gè)與之關(guān)聯(lián)的鎖。這種鎖被稱為監(jiān)視器?(?Monitor?)?或者內(nèi)部鎖?(?Intrinsic?Lock?)。內(nèi)部鎖是一種排他鎖,它能夠保障原子性、可見性和有序性。

內(nèi)部鎖是通過synchronized關(guān)鍵字實(shí)現(xiàn)的。synchronized?關(guān)鍵字可以用來修飾方法以及代碼塊(?花括號(hào)?“?{?}?”?包裹的代碼?)。

synchronized關(guān)鍵字修飾的方法就被稱為同步方法(?Synchronized?Method?)。synchronized??修飾的靜態(tài)方法就被稱為同步靜態(tài)方法,synchronized??修飾的實(shí)例方法就被稱為同步實(shí)例方法。同步方法的整個(gè)方法體就是一個(gè)臨界區(qū)。

synchronized關(guān)鍵字所引導(dǎo)的代碼塊就是臨界區(qū)。鎖句柄是一個(gè)對(duì)象的引用(它或者能夠返回對(duì)象的表達(dá)式)。例如,鎖句柄可以填寫為this關(guān)鍵字(?表示當(dāng)前對(duì)象?)。習(xí)慣上我們也直接稱鎖句柄為鎖。鎖句柄對(duì)應(yīng)的監(jiān)視器就被稱為相應(yīng)同步塊的引導(dǎo)鎖。相應(yīng)地,我們稱呼相應(yīng)的同步塊為該鎖引導(dǎo)的同步塊。

作為鎖句柄的變量通常采用final修飾。這是因?yàn)殒i句柄變量的值一旦改變,會(huì)導(dǎo)致執(zhí)行同一個(gè)同步塊的多個(gè)線程實(shí)際上使用不同的鎖,從而導(dǎo)致競態(tài)。有鑒于此,通常我們會(huì)使用?private?修飾作為鎖句柄的變量。

線程在執(zhí)行臨界區(qū)代碼的時(shí)候必須持有該臨界區(qū)的引導(dǎo)鎖。一個(gè)線程執(zhí)行到同步塊(同步方法也可看作同步塊)時(shí)必須先申請?jiān)撏綁K的引導(dǎo)鎖,只有申請成功(獲得)該鎖的線程才能夠執(zhí)行相應(yīng)的臨界區(qū)。一個(gè)線程執(zhí)行完臨界區(qū)代碼后引導(dǎo)該臨界區(qū)的鎖就會(huì)被自動(dòng)釋放。在這個(gè)過程中,線程對(duì)內(nèi)部鎖的申請與釋放的動(dòng)作由Java虛擬機(jī)負(fù)責(zé)代為實(shí)施,這也正是?synchronized?實(shí)現(xiàn)的鎖被稱為內(nèi)部鎖的原因。

內(nèi)部鎖的使用并不會(huì)導(dǎo)致鎖世漏。這是因?yàn)镴ava編譯器?(?javac?)?在將同步塊代碼編譯為字節(jié)碼的時(shí)候,對(duì)臨界區(qū)中可能拋出的而程序代碼中又未捕獲的異常進(jìn)行了特殊(?代為?)處理,這使得臨界區(qū)的代碼即使拋出異常也不會(huì)妨礙內(nèi)部鎖的釋放。

內(nèi)部鎖的調(diào)度

Java虛擬機(jī)會(huì)為每個(gè)內(nèi)部鎖分配一個(gè)入口集?(?Entry??Set?),用于記錄等待獲得相應(yīng)內(nèi)部鎖的線程。多個(gè)線程申請同一個(gè)鎖的時(shí)候,只有一個(gè)申請者能夠成為該鎖的持有線程(?即申請鎖的操作成功?),而其他申請者的申請操作會(huì)失敗。這些申請失敗的線程并不會(huì)拋出異常,而是會(huì)被暫停(?生命周期狀態(tài)變?yōu)?BLOCKED?)?并被存入相應(yīng)鎖的入口集中等待再次申請鎖的機(jī)會(huì)?。入口集中的線程就被稱為相應(yīng)內(nèi)部鎖的等待線程。當(dāng)這些線程申請的鎖被其持有線程釋放的時(shí)候,該鎖的入口集中的一個(gè)任意線程會(huì)被Java虛擬機(jī)喚醒,從而得到再次申請鎖的機(jī)會(huì)。由于Java?虛擬機(jī)對(duì)內(nèi)部鎖的調(diào)度僅支持非公平調(diào)度,被喚醒的等待線程占用處理器運(yùn)行時(shí)可能還有其他新的活躍線程?(?處于RUNNABLE?狀態(tài),且未進(jìn)入過入口集?)?與該線程搶占這個(gè)被釋放鎖,因此被喚醒的線程不一定就能成為該鎖的持有線程。另外,Java?虛擬機(jī)如何從一個(gè)鎖的入口集中選擇一個(gè)等待線程,作為下一個(gè)可以參與再次申請相應(yīng)鎖的線程,這個(gè)細(xì)節(jié)與?Java?虛擬機(jī)的具體實(shí)現(xiàn)有關(guān):這個(gè)被選中的線程有可能是入口集中等待時(shí)間最長的線程,也可能是等待時(shí)間最短的線程,或者完全是隨機(jī)的一個(gè)線程。因此,我們不能依賴這個(gè)具體的選擇算法。

前文我們講解鎖是如何保證可見性的時(shí)候提到了線程獲得和釋放鎖時(shí)所分別執(zhí)行的兩個(gè)動(dòng)作:刷新處理器緩存和沖刷處理器緩存。對(duì)于同一個(gè)鎖所保護(hù)的共享數(shù)據(jù)而言,前一個(gè)動(dòng)作保證了該鎖的當(dāng)前持有線程能夠讀取到前一個(gè)持有線程對(duì)這些數(shù)據(jù)所做的更新,后一個(gè)動(dòng)作保證了該鎖的持有線程對(duì)這些數(shù)據(jù)所做的更新對(duì)該鎖的后續(xù)持有線程可見。那么,這兩個(gè)動(dòng)作是如何實(shí)現(xiàn)的呢?弄清楚這個(gè)問題有助于我們學(xué)習(xí)和掌握包括鎖在內(nèi)的所有Java線程同步機(jī)制?。

Java虛擬機(jī)底層實(shí)際上是借助內(nèi)存屏障(?Memory?Barrier?,也稱?Fence?)來實(shí)現(xiàn)上述兩個(gè)動(dòng)作的。內(nèi)存屏障是對(duì)一類僅針對(duì)內(nèi)存讀、寫操作指令?(?Instruction?)?的跨處理器架構(gòu)?(?比如?x86?、ARM?)的比較底層的抽象(?或者稱呼?)。內(nèi)存屏障是被插入到兩個(gè)指令之間進(jìn)行使用的,其作用是禁止編譯器、處理器重排序從而保障有序性。它在指令序列?(?如指令?1?;指令2?;指令3?)中就像是一堵墻?(?因此被稱為屏障?)一樣使其兩側(cè)?(?之前和之后?)的指令無法“穿越”它?(?一旦穿越了就是重排序了?)。但是,為了實(shí)現(xiàn)禁止重排序的功能,這些指令也往往具有一個(gè)副作用刷新處理器緩存、沖刷處理器緩存,從而保證可見性。不同微架構(gòu)的處理器所提供的這樣的指令是不同的,并且出于不同的目的使用的相應(yīng)指令也是不同的。例如對(duì)于?“寫-寫”?(?寫后寫?)?操作,如果僅僅是為了防止?(?禁止?)?重排序而對(duì)可見性保障沒有要求,那么在x86架構(gòu)的處理器下使用空操作就可以了(??因?yàn)?x86處理器不會(huì)對(duì)?“寫-寫”?操作進(jìn)行重排序?)。而如果對(duì)可見性有要求(比如前一個(gè)寫操作的結(jié)果要在后一個(gè)寫操作執(zhí)行前對(duì)其他處理器可見),那么在x86????處理器下需要使用LOCK?前綴指令或者sfence?指令、mfence?指令;在?ARM?處理器下則需要使用?DMB?指令。

按照內(nèi)存屏障所起的作用來劃分,將內(nèi)存屏障劃分為以下幾種。

按照可見性保障來劃分。內(nèi)存屏障可分為加載屏障(Load?Barrier)和存儲(chǔ)屏障(Store??????Barrier)。加載屏障的作用是刷新處理器緩存,存儲(chǔ)屏障的作用沖刷處理器緩存。Java虛擬機(jī)會(huì)在?MonitorExit?(?釋放鎖?)?對(duì)應(yīng)的機(jī)器碼指令之后插入一個(gè)存儲(chǔ)屏障,這就保障了寫線程在釋放鎖之前在臨界區(qū)中對(duì)共享變量所做的更新對(duì)讀線程的執(zhí)行處理器來說是可同步的。相應(yīng)地,Java?虛擬機(jī)會(huì)在?MonitorEnter?(?申請鎖?)?對(duì)應(yīng)的機(jī)器碼指令之后臨界區(qū)開始之前的地方插入一個(gè)加載屏障,這使得讀線程的執(zhí)行處理器能夠?qū)懢€程對(duì)相應(yīng)共享變量所做的更新從其他處理器同步到該處理器的高速緩存中。因此,可見性的保障是通過寫線程和讀線程成對(duì)地使用存儲(chǔ)屏障和加載屏障實(shí)現(xiàn)的。

按照有序性保障來劃分,內(nèi)存屏障可以分為獲取屏障(Acquire?Barrier)和釋放屏障?(?Release?Barrier?)。獲?取?屏?障?的?使?用?方?式?是?在?一?個(gè)?讀?操?作?(?包括?Read-Modify-Write?以及普通的讀操作?)之后插入該內(nèi)存屏障,其作用是禁止該讀操作與其后的任何讀寫操作之間進(jìn)行重排序,這相當(dāng)于在進(jìn)行后續(xù)操作之前先要獲得相應(yīng)共享數(shù)據(jù)的所有權(quán)?(?這也是該屏障的名稱來源?)。釋放屏障的使用方式是在一個(gè)寫操作之前插入該內(nèi)存屏障,其作用是禁止該寫操作與其前面的任何讀寫操作之間進(jìn)行重排序。這相當(dāng)于在對(duì)相應(yīng)共享數(shù)據(jù)操作結(jié)束后釋放所有權(quán)(?這也是該屏障的名稱來源?)。?Java虛擬機(jī)會(huì)在?MonitorEnter(?它包含了讀操作?)?對(duì)應(yīng)的機(jī)器碼指令之后臨界區(qū)開始之前的地方插入一個(gè)獲取屏障,并在臨界區(qū)結(jié)束之后?MonitorExit?(?它包含了寫操作?)?對(duì)應(yīng)的機(jī)器碼指令之前的地方插入一個(gè)釋放屏障。因此,這兩種屏障就像是三明治的兩層面包片把火腿夾住一樣把臨界區(qū)中的代碼(指令序列)包括起來,如圖所示。

由于獲取屏障禁止了臨界區(qū)中的任何讀、寫操作被重排序到臨界區(qū)之前的可能性。而釋放屏障又禁止了臨界區(qū)中的任何讀、寫操作被重排序到臨界區(qū)之后的可能性。因此臨界區(qū)內(nèi)的任何讀、寫操作都無法被重排序到臨界區(qū)之外。在鎖的排他性的作用下,這使得臨界區(qū)中執(zhí)行的操作序列具有原子性。因此,寫線程在臨界區(qū)中對(duì)各個(gè)共享變量所做的更新會(huì)同時(shí)對(duì)讀線程可見,即在賣線程看來各個(gè)共享變量就像是“一下子”?被更新的,于是這些線程無從?(?也無必要?)?區(qū)分這些共享變量是以何種順序被更新的。這使得寫線程在臨界區(qū)中執(zhí)行的操作自然而然地具有有序性讀線程對(duì)這些操作的感知順序與源代碼順序一致。

可見,鎖對(duì)有序性的保障是通過寫線程和讀線程配對(duì)使用釋放屏障與加載屏障實(shí)現(xiàn)的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容