引自《美團(tuán)點(diǎn)評(píng)團(tuán)隊(duì)——不可不說(shuō)的Java“鎖事”》
1.前言
? ? ? ?Java中往往按照是否含有某一特性來(lái)定義鎖瑞凑,我們通過(guò)特性將鎖進(jìn)行分組歸類彻磁,如下圖所示:
2.樂(lè)觀鎖VS悲觀鎖
? ? ? ?對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,悲觀鎖認(rèn)為自己在使用數(shù)據(jù)的時(shí)候一定有別的線程來(lái)修改數(shù)據(jù),因此在獲取數(shù)據(jù)的時(shí)候會(huì)先加鎖,確保數(shù)據(jù)不會(huì)被別的線程修改。
? ? ? ?Java中肺稀,synchronized關(guān)鍵字和Lock的實(shí)現(xiàn)類都是悲觀鎖。
? ? ? ?而樂(lè)觀鎖認(rèn)為自己在使用數(shù)據(jù)時(shí)不會(huì)有別的線程修改數(shù)據(jù)应民,所以不會(huì)添加鎖话原,只是在更新數(shù)據(jù)的時(shí)候去判斷之前有沒(méi)有別的線程更新了這個(gè)數(shù)據(jù)。如果這個(gè)數(shù)據(jù)沒(méi)有被更新诲锹,當(dāng)前線程將自己修改的數(shù)據(jù)成功寫入繁仁。如果數(shù)據(jù)已經(jīng)被其他線程更新,則根據(jù)不同的實(shí)現(xiàn)方式執(zhí)行不同的操作(例如報(bào)錯(cuò)或自動(dòng)重試)归园。
? ? ? ?樂(lè)觀鎖在Java中是通過(guò)無(wú)鎖編程來(lái)實(shí)現(xiàn)黄虱,最常曹勇CAS算法,Java原子類中的遞增操作就通過(guò)CAS自旋實(shí)現(xiàn)的庸诱。
? ? ? ?通過(guò)兩者特性可以得出:
- 悲觀鎖適合寫操作多的場(chǎng)景捻浦,先加鎖可以保證寫操作時(shí)數(shù)據(jù)正確;
-
樂(lè)觀鎖適合讀操作多的場(chǎng)景桥爽,不加鎖能夠使其讀操作性能大幅提升朱灿。
image.png
? ? ? ?為什么樂(lè)觀鎖能夠做到不鎖定同步資源也可以正確實(shí)現(xiàn)線程同步呢?我們通過(guò)介紹樂(lè)觀鎖的主要實(shí)現(xiàn)方式“CAS”的技術(shù)原理來(lái)回答钠四。
? ? ? ?CAS(Compare And Swap盗扒,比較與交換),是一種無(wú)鎖算法缀去。在不適用鎖(沒(méi)有線程阻塞)的情況下實(shí)現(xiàn)多線程之間的變量同步侣灶。java.util.concurrent包中的原子類就是通過(guò)CAS實(shí)現(xiàn)了樂(lè)觀鎖。
? ? ? ?CAS算法設(shè)計(jì)三個(gè)操作數(shù):
- 需要讀寫的內(nèi)存值V缕碎。
- 進(jìn)行比較的值A(chǔ)炫隶。
- 要寫入的新值B。
當(dāng)且僅當(dāng)V的值等于A時(shí)阎曹,CAS通過(guò)原子方式用新值B來(lái)更新V的值,否則不執(zhí)行任何操作煞檩。一般情況下处嫌,“更新”是一個(gè)不斷重試的操作。
? ? ? ?CAS雖然很高效斟湃,但是存在三大問(wèn)題:
- ABA問(wèn)題
- 循環(huán)時(shí)間長(zhǎng)開(kāi)銷大:CAS操作如果長(zhǎng)時(shí)間不成功熏迹,會(huì)導(dǎo)致其一直自旋,給CPU帶來(lái)非常大的開(kāi)銷凝赛。
- 只能保證一個(gè)共享變量的原子操作
3.自旋鎖 VS 適應(yīng)性自旋鎖
? ? ? ?以下為一些前提知識(shí):
? ? ? ?阻塞或喚醒一個(gè)Java線程需要操作系統(tǒng)切換CPU狀態(tài)來(lái)完成注暗,這種狀態(tài)轉(zhuǎn)換需要耗費(fèi)處理器時(shí)間坛缕。如果同步代碼塊中的內(nèi)容過(guò)于簡(jiǎn)單,狀態(tài)轉(zhuǎn)換消耗的時(shí)間有可能比用戶代碼執(zhí)行的時(shí)間還要長(zhǎng)捆昏。
? ? ? ?很多場(chǎng)景中赚楚,同步資源的鎖定時(shí)間很短,為了一小段時(shí)間去切換線程骗卜,其花費(fèi)可能會(huì)得不償失宠页。如果物理機(jī)器有多個(gè)處理器,能夠讓兩個(gè)或以上的線程同步并行執(zhí)行寇仓,我們可以讓后面那個(gè)請(qǐng)求鎖的線程不放棄CPU的執(zhí)行時(shí)間举户,看看持有鎖的線程是否很快就會(huì)釋放鎖。
? ? ? ?為了讓當(dāng)前線程“稍等一下”遍烦,需讓其進(jìn)行自旋俭嘁,如果在自旋完成后前面鎖定同步資源的線程已經(jīng)釋放了鎖,那么當(dāng)前線程就可以不必阻塞而是直接獲取同步資源服猪,從而避免切換線程的開(kāi)銷供填。這就是自旋鎖。
? ? ? ?自旋鎖不能代替阻塞蔓姚,雖然其避免了線程切換的開(kāi)銷捕虽,但是它占用處理器時(shí)間。如果鎖被占用的時(shí)間很短坡脐,自旋等待的效果會(huì)非常好泄私。反之,自旋的線程會(huì)白白浪費(fèi)掉處理器資源备闲。所以晌端,自旋時(shí)間應(yīng)該有限度,如果超過(guò)限定次數(shù)(默認(rèn)10次恬砂,-XX:PreBlockSpin來(lái)更改)沒(méi)有成功獲得鎖咧纠,就應(yīng)掛起線程。
? ? ? ?自旋鎖的實(shí)現(xiàn)原理也是CAS泻骤。在JDK 6中默認(rèn)開(kāi)啟漆羔,且引入了自適應(yīng)自旋鎖。
? ? ? ?自適應(yīng)意味著自旋的時(shí)間(次數(shù))不再固定狱掂,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定演痒。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過(guò)鎖趋惨,且持有鎖的線程正在運(yùn)行中鸟顺,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功,進(jìn)而允許自旋等待持續(xù)相對(duì)長(zhǎng)的時(shí)間器虾。如果對(duì)于某個(gè)鎖讯嫂,自旋很少成功獲得過(guò)蹦锋,那在以后嘗試獲取這個(gè)鎖時(shí)可能省略掉自旋過(guò)程,直接阻塞線程欧芽,避免浪費(fèi)處理器資源莉掂。
4.無(wú)鎖VS偏向鎖VS輕量級(jí)鎖VS重量級(jí)鎖
? ? ? ?這四種指的是鎖的狀態(tài),專門針對(duì)synchronized渐裸。
? ? ? ?前提知識(shí):
- 為什么synchronized能實(shí)現(xiàn)線程同步巫湘?我們需要了解兩個(gè)重要的概念(“Java對(duì)象頭,“Monitor””)
Java對(duì)象頭
? ? ? ?synchronized是悲觀鎖昏鹃,在操作同步資源之前需要給同步資源先加鎖尚氛,這把鎖就是存在Java對(duì)象頭中的。Java對(duì)象頭是什么洞渤?
? ? ? ?以Hotspot虛擬機(jī)為例阅嘶,其對(duì)象頭主要包含兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)、Klass Pointer(類型指針)载迄。
Mark Word:默認(rèn)存儲(chǔ)對(duì)象的HashCode讯柔,分代年齡和鎖標(biāo)志位信息。這些信息都是與對(duì)象自身定義無(wú)關(guān)的數(shù)據(jù)护昧,所以Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù)魂迄。它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間,也就是說(shuō)在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化惋耙。
Klass Point:對(duì)象指向它的類元數(shù)據(jù)的指針捣炬,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象時(shí)哪個(gè)類的實(shí)例。
Monitor
? ? ? ?Monitor可以理解為一個(gè)同步工具或一種同步機(jī)制绽榛,通常被描述為一個(gè)對(duì)象湿酸。每個(gè)Java對(duì)象都有一把看不見(jiàn)的鎖,稱為內(nèi)部鎖或Monitor鎖灭美。
? ? ? ?Monitor是線程私有的數(shù)據(jù)結(jié)構(gòu)推溃,每一個(gè)線程都有一個(gè)可用monitor record列表,同時(shí)還有一個(gè)全局的可用列表届腐。每一個(gè)被鎖住的對(duì)象都會(huì)和一個(gè)monitor對(duì)象關(guān)聯(lián)铁坎,同時(shí)monitor中有一個(gè)Owner字段存放擁有該鎖的線程的唯一標(biāo)識(shí),標(biāo)識(shí)該鎖被這個(gè)線程占用犁苏。
? ? ? ?將話題回到synchronized厢呵。synchronized通過(guò)Monitor來(lái)實(shí)現(xiàn)線程同步,Monitor是依賴于底層的操作系統(tǒng)的Mutex Lock(互斥鎖)來(lái)實(shí)現(xiàn)的線程同步傀顾。
? ? ? ?這種依賴于操作系統(tǒng)Mutex Lock所實(shí)現(xiàn)的鎖我們稱之為“重量級(jí)鎖”,JDK 6中為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗碌奉,引入了“偏向鎖”和“輕量級(jí)鎖”短曾。
? ? ? ?目前鎖一共有4種狀態(tài)寒砖,級(jí)別從低到高依次是:無(wú)鎖、偏向鎖嫉拐、輕量級(jí)鎖和重量級(jí)鎖哩都,鎖狀態(tài)只能升級(jí)不能降級(jí)。
? ? ? ?以下是四種鎖狀態(tài)對(duì)應(yīng)的Mark Word內(nèi)容:
無(wú)鎖
? ? ? ?無(wú)鎖沒(méi)有對(duì)資源進(jìn)行鎖定婉徘,所有線程都能訪問(wèn)并修改同一個(gè)資源漠嵌,但同時(shí)只有一個(gè)線程能修改成功。
? ? ? ?無(wú)鎖的特點(diǎn)是修改操作在循環(huán)中進(jìn)行盖呼,線程不斷的嘗試修改共享資源儒鹿。如果沒(méi)有沖突就修改成功并退出,否則就會(huì)繼續(xù)循環(huán)嘗試几晤。如果有多個(gè)線程修改同一個(gè)值约炎,必定會(huì)有一個(gè)線程能修改成功,而其他修改失敗的線程會(huì)不斷重試直到修改成功蟹瘾。CAS原理即是無(wú)鎖的實(shí)現(xiàn)圾浅。無(wú)鎖無(wú)法全面代替有鎖,但無(wú)鎖在某些場(chǎng)合下的性能是非常高的憾朴。
偏向鎖
? ? ? ?偏向鎖是指一段同步代碼一直被一個(gè)線程所訪問(wèn)狸捕,那么該線程會(huì)自動(dòng)獲取鎖,降低鎖的代價(jià)众雷。
? ? ? ?多數(shù)情況下灸拍,鎖總是由同一線程多次獲得,不存在多線程競(jìng)爭(zhēng)报腔,所以出現(xiàn)了偏向鎖株搔。其目標(biāo)就是在只有一個(gè)線程執(zhí)行同步代碼塊時(shí)能夠提高性能。
? ? ? ?當(dāng)一個(gè)線程訪問(wèn)同步代碼塊并獲取鎖時(shí)纯蛾,會(huì)在Mark Word里存儲(chǔ)鎖偏向的線程ID纤房。在線程進(jìn)入和退出同步塊時(shí)不再通過(guò)CAS操作來(lái)加鎖和解鎖,而是檢測(cè)Mark Word里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖翻诉。引入偏向鎖時(shí)為了在無(wú)多線程競(jìng)爭(zhēng)的情況下盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑炮姨,因?yàn)檩p量級(jí)鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要再置換ThreadID的時(shí)候依賴一次CAS原子指令即可碰煌。
? ? ? ?偏向鎖只有遇到其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí)舒岸,持有偏向鎖的線程才會(huì)釋放鎖,線程不會(huì)主動(dòng)釋放偏向鎖芦圾。偏向鎖的撤銷蛾派,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒(méi)有字節(jié)碼正在執(zhí)行),它會(huì)首先暫停擁有偏向鎖的線程,判斷鎖對(duì)象是否處于被鎖定狀態(tài)洪乍。撤銷偏向鎖后恢復(fù)到無(wú)鎖(標(biāo)志位為“01”)或輕量級(jí)鎖(標(biāo)志位為“00”)的狀態(tài)眯杏。
? ? ? ?偏向鎖在JDK 6及以后的JVM里是默認(rèn)啟用的】前模可以通過(guò)JVM參數(shù)關(guān)閉偏向鎖:-XX:UseBiasedLocking=false岂贩,關(guān)閉之后程序默認(rèn)進(jìn)入輕量級(jí)鎖狀態(tài)。
輕量級(jí)鎖
? ? ? ?是指當(dāng)鎖是偏向鎖的時(shí)候巷波,被另外的線程所訪問(wèn)萎津,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過(guò)自旋的形式嘗試獲取鎖抹镊,不會(huì)阻塞锉屈,從而提高性能。
? ? ? ?在代碼進(jìn)入同步塊的時(shí)候髓考,如果同步對(duì)象鎖狀態(tài)為無(wú)鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài)部念,是否為偏向鎖為“0”),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間氨菇,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝儡炼,然后拷貝對(duì)象頭中的Mark Word復(fù)制到鎖記錄中。
? ? ? ?拷貝成功后查蓉,虛擬機(jī)將使用CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指針乌询,并將Lock Record里的owner指針指向?qū)ο蟮腗ark Word。
? ? ? ?如果這個(gè)更新動(dòng)作成功了豌研,那么這個(gè)線程就擁有了該對(duì)象的鎖妹田,并且對(duì)象Mark Word的鎖標(biāo)志位設(shè)置為“00”,表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)鹃共。
? ? ? ?如果輕量級(jí)鎖的更新操作失敗了鬼佣,虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖霜浴,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行晶衷,否則說(shuō)明多個(gè)線程競(jìng)爭(zhēng)鎖。
? ? ? ?若當(dāng)前只有一個(gè)等待線程阴孟,則該線程通過(guò)自旋進(jìn)行等待晌纫。但是當(dāng)自旋超過(guò)一定的次數(shù),或者一個(gè)線程在持有鎖永丝,一個(gè)在自旋锹漱,又有第三個(gè)來(lái)訪時(shí),輕量級(jí)鎖升級(jí)為重量級(jí)鎖慕嚷。
重量級(jí)鎖
? ? ? ?升級(jí)為重量級(jí)鎖時(shí)哥牍,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”毕泌,此時(shí)Mark Word中存儲(chǔ)的是指向重量級(jí)鎖的指針,此時(shí)等待鎖的線程都會(huì)進(jìn)入阻塞狀態(tài)砂心。
? ? ? ?整體鎖狀態(tài)升級(jí)流程如下:
? ? ? ?綜上懈词,偏向鎖通過(guò)對(duì)比Mark Word解決加鎖問(wèn)題,避免執(zhí)行CAS操作辩诞。而輕量級(jí)鎖是通過(guò)用CAS操作和自旋來(lái)解決加鎖問(wèn)題,避免線程阻塞和喚醒而影響性能纺涤。重量級(jí)鎖是將除了擁有鎖的線程以外的線程都阻塞译暂。
5.公平鎖VS非公平鎖
? ? ? ?公平鎖是指多個(gè)線程按照申請(qǐng)鎖的順序來(lái)獲取鎖,線程直接進(jìn)入隊(duì)列中排隊(duì)撩炊,隊(duì)列中的第一個(gè)線程才能獲得鎖外永。其優(yōu)點(diǎn)使等待鎖的線程不會(huì)餓死,缺點(diǎn)是整體吞吐效率比非公平鎖要低拧咳,等待隊(duì)列中除第一個(gè)線程以外的所有線程都會(huì)阻塞伯顶,CPU喚醒阻塞線程的開(kāi)銷比非公平鎖大。
? ? ? ?非公平鎖是多個(gè)線程加鎖時(shí)直接嘗試獲取鎖骆膝,獲取不到才會(huì)到等待隊(duì)列的隊(duì)尾等待祭衩。但如果此時(shí)鎖剛好可用,這個(gè)線程可以無(wú)需阻塞直接獲取到鎖阅签,所以非公平鎖可能出現(xiàn)后申請(qǐng)鎖的線程先獲取鎖的場(chǎng)景掐暮。非公平鎖的優(yōu)點(diǎn)是可以減少喚起線程的開(kāi)銷,整體的吞吐效率高政钟,因?yàn)榫€程有幾率不阻塞直接獲得鎖路克,CPU不必喚醒所有線程。缺點(diǎn)是處于等待隊(duì)列中的線程可能會(huì)餓死养交,或者等很久才會(huì)獲得鎖精算。
? ? ? ?下圖為公平鎖圖示:
? ? ? ?下圖為非公平鎖圖示:
6.可重入鎖VS非可重入鎖
? ? ? ?可重入鎖又名遞歸鎖,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候碎连,再進(jìn)入該線程的內(nèi)層方法會(huì)自動(dòng)獲取鎖(前提鎖對(duì)象是同一個(gè)對(duì)象或class)灰羽,不會(huì)因?yàn)橹耙呀?jīng)獲取過(guò)還沒(méi)釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖破花,其優(yōu)點(diǎn)是可一定程度避免死鎖谦趣。
? ? ? ?為什么可重入鎖在嵌套調(diào)用時(shí)可以自動(dòng)獲取鎖呢?
? ? ? ?但如果是非可重入鎖的話座每,此時(shí)管理員只允許鎖和同一個(gè)人的一個(gè)水桶綁定前鹅。第一個(gè)水桶和鎖綁定打完水之后并不會(huì)釋放鎖,導(dǎo)致第二個(gè)水桶不能和鎖綁定也無(wú)法打水峭梳。當(dāng)前線程出現(xiàn)死鎖舰绘,整個(gè)等待隊(duì)列中的所有線程都無(wú)法被喚醒蹂喻。
? ? ? ?可重入鎖和非可重入鎖在Java中的實(shí)現(xiàn)分別是ReentrantLock和NonReentrantLock。
7.獨(dú)享鎖VS共享鎖
? ? ? ?獨(dú)享鎖和共享鎖在Java中的實(shí)現(xiàn)分別是ReentrantLock和ReentrantReadWriteLock捂寿。
? ? ? ?獨(dú)享鎖也叫排他鎖口四,是指該鎖一次只能被一個(gè)線程所持有。如果線程T對(duì)數(shù)據(jù)A加上排它鎖后秦陋,其他線程不能再對(duì)A加任何類型的鎖蔓彩。獲得排他鎖的線程既能讀數(shù)據(jù)又能修改數(shù)據(jù)。
? ? ? ?共享鎖是指該鎖可以被多個(gè)線程所持有驳概。如果線程T對(duì)數(shù)據(jù)A加上共享鎖后赤嚼,其他線程只能對(duì)A再加共享鎖,不能加排它鎖顺又。獲得共享鎖的線程只能讀數(shù)據(jù)更卒,不能修改數(shù)據(jù)。
? ? ? ?獨(dú)享鎖與共享鎖也是通過(guò)AQS來(lái)實(shí)現(xiàn)的稚照,通過(guò)實(shí)現(xiàn)不同的方法蹂空,來(lái)實(shí)現(xiàn)獨(dú)享或者共享。