鎖概述
我們知道線程安全問題的產(chǎn)生前提是多個線程并發(fā)訪問共享變量让歼、共享資源(以下統(tǒng)稱為共享數(shù)據(jù))。于是丽啡,我們很容易想到保障線程安全的方法將多個線程對共享數(shù)據(jù)的并發(fā)訪問轉(zhuǎn)換為串行訪問谋右,即一個共享數(shù)據(jù)一次只能被一個線程訪問,該線程訪問結(jié)束后其他線程才能對其進行訪問补箍。鎖(Lock)就是利用這種思路以保障線程安全的線程同步機制倚评。
按照上述思路,鎖可以理解為對共享數(shù)據(jù)進行保護的許可證馏予。對于同一個許可證所保護的共享數(shù)據(jù)而言天梧,任何線程訪問這些共享數(shù)據(jù)前必須先持有該許可證。一個線程只有在持有許可證的情況下才能夠?qū)@些共享數(shù)據(jù)進行訪問霞丧;并且呢岗,一個許可證一次只能夠被一個線程持有;許可證的持有線程在其結(jié)束對這些共享數(shù)據(jù)的訪問后必須讓出(釋放)其持有的許可證蛹尝,以便其他線程能夠?qū)@些共享數(shù)據(jù)進行訪問后豫。
一個線程在訪問共享數(shù)據(jù)前必須申請相應(yīng)的鎖(許可證),線程的這個動作被稱為鎖的獲得(Acquire)突那。一個線程獲得某個鎖(?持有許可證?)挫酿,我們就稱該線程為相應(yīng)鎖的持有線程?(?線程持有許可證?),一個鎖一次只能被一個線程持有愕难。鎖的持有線程可以對該鎖所保護的共享數(shù)據(jù)進行訪問早龟,訪問結(jié)束后該線程必須釋放?(?Release?)?相應(yīng)的鎖。鎖的持有線程在其獲得鎖之后和釋放鎖之前這段時間內(nèi)所執(zhí)行的代碼被稱為臨界區(qū)(?Critical?Section?)猫缭。因此葱弟,共享數(shù)據(jù)只允許在臨界區(qū)內(nèi)進行訪問,臨界區(qū)一次只能被一個線程執(zhí)行猜丹。
鎖具有排他性(Exclusive)芝加,即一個鎖一次只能被一個線程持有。因此射窒,這種鎖被稱為排他鎖或者互斥鎖?(?Mutex?)藏杖。這種鎖的實現(xiàn)方式代表了鎖的基本原理,如圖所示脉顿。?
按照Java虛擬機對鎖的實現(xiàn)方式劃分蝌麸,Java?平臺中的鎖包括內(nèi)部鎖(?Intrinsic??Lock?)和顯式鎖?(?Explicit?Lock?)。內(nèi)部鎖是通過synchronized關(guān)鍵字實現(xiàn)的弊予;顯式鎖是通過java.concurrent.locks.Lock接口的實現(xiàn)類(如?java.concurrent.locks.ReentrantLock?類?)?實現(xiàn)的祥楣。
鎖的作用
鎖能夠保護共享數(shù)據(jù)以實現(xiàn)線程安全,其作用包括保障原子性汉柒、保障可見性和保障有序性误褪。
鎖是通過互斥保障原子性的。所謂互斥 ( Mutual Exclusion )碾褂,就是指一個鎖一次只能被一個線程持有兽间。因此一個線程持有一個鎖的時候,其他線程無法獲得該鎖正塌,而只能等待其釋放該鎖后再申請嘀略。這就保證了臨界區(qū)代碼一次只能夠被一個線程執(zhí)行。因此乓诽,一個線程執(zhí)行臨界區(qū)期間沒有其他線程能夠訪問相應(yīng)的共享數(shù)據(jù)帜羊,這使得臨界區(qū)代碼所執(zhí)行的操作自然而然地具有不可分割的特性,即具備了原子性鸠天。
從互斥的角度來看讼育,鎖其實是將多個線程對共享數(shù)據(jù)的訪問由本來的并發(fā)( 未使用鎖的情況下 )改為串行( 使用鎖之后 )。因此稠集,雖然實現(xiàn)并發(fā)是多線程編程的目標奶段,但是這種并發(fā)往往是并發(fā)中帶有串行的局部并發(fā)。這好比公路維修使得多股車道在某處被合并成一股小車道剥纷,從而使原本在多股車道上并駕齊驅(qū)的車輛不得不“魚貫而行”痹籍。
我們知道,可見性的保障是通過寫線程沖刷處理器緩存和讀線程刷新處理器緩存這兩個動作實現(xiàn)的晦鞋。在Java平臺中蹲缠,鎖的獲得隱含著刷新處理器緩存這個動作,這使得讀線程在執(zhí)行臨界區(qū)代碼前( 獲得鎖之后 ) 可以將寫線程對共享變量所做的更新同步到該線程執(zhí)行處理器的高速緩存中悠垛;而鎖的釋放隱含著沖刷處理器緩存這個動作吼砂,這使得寫線程對共享變量所做的更新能夠被“推送” 到該線程執(zhí)行處理器的高速緩存中,從而對讀線程可同步鼎文。因此渔肩,鎖能夠保障可見性。
鎖能夠保障有序性拇惋。寫線程在臨界區(qū)中所執(zhí)行的一系列操作在讀線程所執(zhí)行的臨界區(qū)看起來像是完全按照源代碼順序執(zhí)行周偎,即讀線程對這些操作的感知順序與源代碼順序一致。這是暫且對原子性和可見性的保障的結(jié)果撑帖。設(shè)寫線程在臨界區(qū)中更新了b 蓉坎、c 和 flag 這 3 個共享變量,如下代碼片段所示 :
由于鎖對可見性的保障胡嘿,寫線程在臨界區(qū)中對上述任何一個共享變量所做的更新都對讀線程可見蛉艾。并且,由于臨界區(qū)內(nèi)的操作具有原子性,因此寫線程對上述共事變量的更新會同時對讀線程可見勿侯,即在讀線程看來這些變量就像是在同一刻被更新的拓瞪。因此讀線程并無法(也沒有必要)區(qū)分寫線程實際上是以什么順序更新上述變量的,這意味著讀線程可以認為寫線程是依照源代碼順序更新上述共享變量的助琐,即有序性得以保障祭埂。
盡管鎖能夠保障有序性,但是這并不意味著臨界區(qū)內(nèi)的內(nèi)存操作不能夠被重排序兵钮。臨界區(qū)內(nèi)的任意兩個操作依然可以在臨界區(qū)之內(nèi)被重排序(即不會重排到臨界區(qū)之外)蛆橡。由于臨界區(qū)內(nèi)的操作具有的原子性,寫線程在臨界區(qū)內(nèi)對各個共享數(shù)據(jù)的更新同時對讀線程可見掘譬,因此這種重排序并不會對其他線程產(chǎn)生影響泰演。
在理解,以及使用鎖保證線程安全的時候葱轩,需要注意鎖對可見性睦焕、原子性和有序性的保障是有條件的,我們要同時保證以下兩點得以滿足酿箭。
? 這些線程在訪問同一組共享數(shù)據(jù)的時候必須使用同一個鎖复亏。
? 這些線程中的任意一個線程,即使其僅僅是讀取這組共享數(shù)據(jù)而沒有對其進行更新的話缭嫡,也需要在讀取時持有相應(yīng)的鎖缔御。
上述任意一個條件未滿足都會使原子性、可見性和有序性沒有保障妇蛀「唬可見,我們說鎖能夠保護共享數(shù)據(jù)其實是一種“協(xié)議”?的結(jié)果评架,這個協(xié)議就是任何訪問該共享數(shù)據(jù)的寫線程眷茁、?讀線程都要滿足上述條件。只要有任何一個線程沒有遵守這個協(xié)議實際上就被打破纵诞,從而無法保障線程安全上祈。這就好比交通規(guī)則(?“協(xié)議”?)?要靠人人都遵守才能保障交通安全一樣。
Java平臺中的任何一個對象都有唯一一個與之關(guān)聯(lián)的鎖浙芙。這種鎖被稱為監(jiān)視器?(?Monitor?)?或者內(nèi)部鎖?(?Intrinsic?Lock?)登刺。內(nèi)部鎖是一種排他鎖,它能夠保障原子性嗡呼、可見性和有序性纸俭。
內(nèi)部鎖是通過synchronized關(guān)鍵字實現(xiàn)的。synchronized?關(guān)鍵字可以用來修飾方法以及代碼塊(?花括號?“?{?}?”?包裹的代碼?)南窗。
synchronized關(guān)鍵字修飾的方法就被稱為同步方法(?Synchronized?Method?)揍很。synchronized??修飾的靜態(tài)方法就被稱為同步靜態(tài)方法郎楼,synchronized??修飾的實例方法就被稱為同步實例方法。同步方法的整個方法體就是一個臨界區(qū)窒悔。
synchronized關(guān)鍵字所引導(dǎo)的代碼塊就是臨界區(qū)呜袁。鎖句柄是一個對象的引用(它或者能夠返回對象的表達式)。例如蛉迹,鎖句柄可以填寫為this關(guān)鍵字(?表示當(dāng)前對象?)傅寡。習(xí)慣上我們也直接稱鎖句柄為鎖放妈。鎖句柄對應(yīng)的監(jiān)視器就被稱為相應(yīng)同步塊的引導(dǎo)鎖北救。相應(yīng)地,我們稱呼相應(yīng)的同步塊為該鎖引導(dǎo)的同步塊芜抒。
作為鎖句柄的變量通常采用final修飾珍策。這是因為鎖句柄變量的值一旦改變,會導(dǎo)致執(zhí)行同一個同步塊的多個線程實際上使用不同的鎖宅倒,從而導(dǎo)致競態(tài)攘宙。有鑒于此,通常我們會使用?private?修飾作為鎖句柄的變量拐迁。
線程在執(zhí)行臨界區(qū)代碼的時候必須持有該臨界區(qū)的引導(dǎo)鎖蹭劈。一個線程執(zhí)行到同步塊(同步方法也可看作同步塊)時必須先申請該同步塊的引導(dǎo)鎖,只有申請成功(獲得)該鎖的線程才能夠執(zhí)行相應(yīng)的臨界區(qū)线召。一個線程執(zhí)行完臨界區(qū)代碼后引導(dǎo)該臨界區(qū)的鎖就會被自動釋放铺韧。在這個過程中,線程對內(nèi)部鎖的申請與釋放的動作由Java虛擬機負責(zé)代為實施缓淹,這也正是?synchronized?實現(xiàn)的鎖被稱為內(nèi)部鎖的原因哈打。
內(nèi)部鎖的使用并不會導(dǎo)致鎖世漏。這是因為Java編譯器?(?javac?)?在將同步塊代碼編譯為字節(jié)碼的時候讯壶,對臨界區(qū)中可能拋出的而程序代碼中又未捕獲的異常進行了特殊(?代為?)處理料仗,這使得臨界區(qū)的代碼即使拋出異常也不會妨礙內(nèi)部鎖的釋放。
內(nèi)部鎖的調(diào)度
Java虛擬機會為每個內(nèi)部鎖分配一個入口集?(?Entry??Set?)伏蚊,用于記錄等待獲得相應(yīng)內(nèi)部鎖的線程立轧。多個線程申請同一個鎖的時候,只有一個申請者能夠成為該鎖的持有線程(?即申請鎖的操作成功?)躏吊,而其他申請者的申請操作會失敗氛改。這些申請失敗的線程并不會拋出異常,而是會被暫停(?生命周期狀態(tài)變?yōu)?BLOCKED?)?并被存入相應(yīng)鎖的入口集中等待再次申請鎖的機會?颜阐。入口集中的線程就被稱為相應(yīng)內(nèi)部鎖的等待線程平窘。當(dāng)這些線程申請的鎖被其持有線程釋放的時候,該鎖的入口集中的一個任意線程會被Java虛擬機喚醒凳怨,從而得到再次申請鎖的機會瑰艘。由于Java?虛擬機對內(nèi)部鎖的調(diào)度僅支持非公平調(diào)度是鬼,被喚醒的等待線程占用處理器運行時可能還有其他新的活躍線程?(?處于RUNNABLE?狀態(tài),且未進入過入口集?)?與該線程搶占這個被釋放鎖紫新,因此被喚醒的線程不一定就能成為該鎖的持有線程均蜜。另外,Java?虛擬機如何從一個鎖的入口集中選擇一個等待線程芒率,作為下一個可以參與再次申請相應(yīng)鎖的線程囤耳,這個細節(jié)與?Java?虛擬機的具體實現(xiàn)有關(guān):這個被選中的線程有可能是入口集中等待時間最長的線程,也可能是等待時間最短的線程偶芍,或者完全是隨機的一個線程充择。因此,我們不能依賴這個具體的選擇算法匪蟀。
前文我們講解鎖是如何保證可見性的時候提到了線程獲得和釋放鎖時所分別執(zhí)行的兩個動作:刷新處理器緩存和沖刷處理器緩存椎麦。對于同一個鎖所保護的共享數(shù)據(jù)而言,前一個動作保證了該鎖的當(dāng)前持有線程能夠讀取到前一個持有線程對這些數(shù)據(jù)所做的更新材彪,后一個動作保證了該鎖的持有線程對這些數(shù)據(jù)所做的更新對該鎖的后續(xù)持有線程可見观挎。那么,這兩個動作是如何實現(xiàn)的呢段化?弄清楚這個問題有助于我們學(xué)習(xí)和掌握包括鎖在內(nèi)的所有Java線程同步機制?嘁捷。
Java虛擬機底層實際上是借助內(nèi)存屏障(?Memory?Barrier?,也稱?Fence?)來實現(xiàn)上述兩個動作的显熏。內(nèi)存屏障是對一類僅針對內(nèi)存讀雄嚣、寫操作指令?(?Instruction?)?的跨處理器架構(gòu)?(?比如?x86?、ARM?)的比較底層的抽象(?或者稱呼?)佃延。內(nèi)存屏障是被插入到兩個指令之間進行使用的现诀,其作用是禁止編譯器、處理器重排序從而保障有序性履肃。它在指令序列?(?如指令?1?仔沿;指令2?;指令3?)中就像是一堵墻?(?因此被稱為屏障?)一樣使其兩側(cè)?(?之前和之后?)的指令無法“穿越”它?(?一旦穿越了就是重排序了?)尺棋。但是封锉,為了實現(xiàn)禁止重排序的功能,這些指令也往往具有一個副作用刷新處理器緩存膘螟、沖刷處理器緩存成福,從而保證可見性。不同微架構(gòu)的處理器所提供的這樣的指令是不同的荆残,并且出于不同的目的使用的相應(yīng)指令也是不同的奴艾。例如對于?“寫-寫”?(?寫后寫?)?操作,如果僅僅是為了防止?(?禁止?)?重排序而對可見性保障沒有要求内斯,那么在x86架構(gòu)的處理器下使用空操作就可以了(??因為?x86處理器不會對?“寫-寫”?操作進行重排序?)蕴潦。而如果對可見性有要求(比如前一個寫操作的結(jié)果要在后一個寫操作執(zhí)行前對其他處理器可見)像啼,那么在x86????處理器下需要使用LOCK?前綴指令或者sfence?指令、mfence?指令潭苞;在?ARM?處理器下則需要使用?DMB?指令忽冻。
按照內(nèi)存屏障所起的作用來劃分,將內(nèi)存屏障劃分為以下幾種此疹。
按照可見性保障來劃分僧诚。內(nèi)存屏障可分為加載屏障(Load?Barrier)和存儲屏障(Store??????Barrier)。加載屏障的作用是刷新處理器緩存蝗碎,存儲屏障的作用沖刷處理器緩存湖笨。Java虛擬機會在?MonitorExit?(?釋放鎖?)?對應(yīng)的機器碼指令之后插入一個存儲屏障,這就保障了寫線程在釋放鎖之前在臨界區(qū)中對共享變量所做的更新對讀線程的執(zhí)行處理器來說是可同步的衍菱。相應(yīng)地赶么,Java?虛擬機會在?MonitorEnter?(?申請鎖?)?對應(yīng)的機器碼指令之后臨界區(qū)開始之前的地方插入一個加載屏障肩豁,這使得讀線程的執(zhí)行處理器能夠?qū)懢€程對相應(yīng)共享變量所做的更新從其他處理器同步到該處理器的高速緩存中脊串。因此,可見性的保障是通過寫線程和讀線程成對地使用存儲屏障和加載屏障實現(xiàn)的清钥。
按照有序性保障來劃分琼锋,內(nèi)存屏障可以分為獲取屏障(Acquire?Barrier)和釋放屏障?(?Release?Barrier?)。獲?取?屏?障?的?使?用?方?式?是?在?一?個?讀?操?作?(?包括?Read-Modify-Write?以及普通的讀操作?)之后插入該內(nèi)存屏障祟昭,其作用是禁止該讀操作與其后的任何讀寫操作之間進行重排序缕坎,這相當(dāng)于在進行后續(xù)操作之前先要獲得相應(yīng)共享數(shù)據(jù)的所有權(quán)?(?這也是該屏障的名稱來源?)。釋放屏障的使用方式是在一個寫操作之前插入該內(nèi)存屏障篡悟,其作用是禁止該寫操作與其前面的任何讀寫操作之間進行重排序谜叹。這相當(dāng)于在對相應(yīng)共享數(shù)據(jù)操作結(jié)束后釋放所有權(quán)(?這也是該屏障的名稱來源?)。?Java虛擬機會在?MonitorEnter(?它包含了讀操作?)?對應(yīng)的機器碼指令之后臨界區(qū)開始之前的地方插入一個獲取屏障搬葬,并在臨界區(qū)結(jié)束之后?MonitorExit?(?它包含了寫操作?)?對應(yīng)的機器碼指令之前的地方插入一個釋放屏障荷腊。因此切揭,這兩種屏障就像是三明治的兩層面包片把火腿夾住一樣把臨界區(qū)中的代碼(指令序列)包括起來制跟,如圖所示执解。
由于獲取屏障禁止了臨界區(qū)中的任何讀退盯、寫操作被重排序到臨界區(qū)之前的可能性春霍。而釋放屏障又禁止了臨界區(qū)中的任何讀语泽、寫操作被重排序到臨界區(qū)之后的可能性端蛆。因此臨界區(qū)內(nèi)的任何讀横媚、寫操作都無法被重排序到臨界區(qū)之外床三。在鎖的排他性的作用下一罩,這使得臨界區(qū)中執(zhí)行的操作序列具有原子性。因此撇簿,寫線程在臨界區(qū)中對各個共享變量所做的更新會同時對讀線程可見聂渊,即在賣線程看來各個共享變量就像是“一下子”?被更新的推汽,于是這些線程無從?(?也無必要?)?區(qū)分這些共享變量是以何種順序被更新的。這使得寫線程在臨界區(qū)中執(zhí)行的操作自然而然地具有有序性讀線程對這些操作的感知順序與源代碼順序一致歧沪。
可見歹撒,鎖對有序性的保障是通過寫線程和讀線程配對使用釋放屏障與加載屏障實現(xiàn)的。