【架構師技巧分享】程序員面試美團:面試官突然問Java “鎖”你應該怎么回答该溯?
Java提供了種類豐富的鎖,每種鎖因其特性的不同别惦,在適當?shù)膱鼍跋履軌蛘宫F(xiàn)出非常高的效率。本文旨在對鎖相關源碼(本文中的源碼來自JDK 8)夫椭、使用場景進行舉例掸掸,為讀者介紹主流鎖的知識點,以及不同的鎖的適用場景蹭秋。
Java中往往是按照是否含有某一特性來定義鎖扰付,我們通過特性將鎖進行分組歸類,再使用對比的方式進行介紹仁讨,幫助大家更快捷的理解相關知識羽莺。下面給出本文內容的總體分類目錄:
1.樂觀鎖 VS 悲觀鎖
樂觀鎖與悲觀鎖是一種廣義上的概念,體現(xiàn)了看待線程同步的不同角度洞豁。在Java和數(shù)據(jù)庫中都有此概念對應的實際應用盐固。
先說概念。對于同一個數(shù)據(jù)的并發(fā)操作丈挟,悲觀鎖認為自己在使用數(shù)據(jù)的時候一定有別的線程來修改數(shù)據(jù)刁卜,因此在獲取數(shù)據(jù)的時候會先加鎖,確保數(shù)據(jù)不會被別的線程修改曙咽。Java中蛔趴,synchronized關鍵字和Lock的實現(xiàn)類都是悲觀鎖。
而樂觀鎖認為自己在使用數(shù)據(jù)時不會有別的線程修改數(shù)據(jù)例朱,所以不會添加鎖孝情,只是在更新數(shù)據(jù)的時候去判斷之前有沒有別的線程更新了這個數(shù)據(jù)鱼蝉。如果這個數(shù)據(jù)沒有被更新,當前線程將自己修改的數(shù)據(jù)成功寫入箫荡。如果數(shù)據(jù)已經被其他線程更新魁亦,則根據(jù)不同的實現(xiàn)方式執(zhí)行不同的操作(例如報錯或者自動重試)。
樂觀鎖在Java中是通過使用無鎖編程來實現(xiàn)菲茬,最常采用的是CAS算法吉挣,Java原子類中的遞增操作就通過CAS自旋實現(xiàn)的。
根據(jù)從上面的概念描述我們可以發(fā)現(xiàn):
悲觀鎖適合寫操作多的場景婉弹,先加鎖可以保證寫操作時數(shù)據(jù)正確睬魂。
樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升镀赌。
光說概念有些抽象氯哮,我們來看下樂觀鎖和悲觀鎖的調用方式示例:
通過調用方式示例,我們可以發(fā)現(xiàn)悲觀鎖基本都是在顯式的鎖定之后再操作同步資源商佛,而樂觀鎖則直接去操作同步資源喉钢。那么,為何樂觀鎖能夠做到不鎖定同步資源也可以正確的實現(xiàn)線程同步呢良姆?我們通過介紹樂觀鎖的主要實現(xiàn)方式“CAS” 的技術原理來為大家解惑肠虽。
CAS全稱 Compare And Swap(比較與交換),是一種無鎖算法玛追。在不使用鎖(沒有線程被阻塞)的情況下實現(xiàn)多線程之間的變量同步税课。java.util.concurrent包中的原子類就是通過CAS來實現(xiàn)了樂觀鎖。
CAS算法涉及到三個操作數(shù):
需要讀寫的內存值V痊剖。
進行比較的值A韩玩。
要寫入的新值B。
當且僅當V的值等于 A 時陆馁,CAS通過原子方式用新值B來更新V的值(“比較+更新”整體是一個原子操作)找颓,否則不會執(zhí)行任何操作。一般情況下叮贩,“更新”是一個不斷重試的操作击狮。
之前提到java.util.concurrent包中的原子類,就是通過CAS來實現(xiàn)了樂觀鎖益老,那么我們進入原子類AtomicInteger的源碼帘不,看一下AtomicInteger的定義:
根據(jù)定義我們可以看出各屬性的作用:
unsafe:獲取并操作內存的數(shù)據(jù)。
valueOffset:存儲value在AtomicInteger中的偏移量杨箭。
value:存儲AtomicInteger的int值寞焙,該屬性需要借助volatile關鍵字保證其在線程間是可見的。
接下來,我們查看AtomicInteger的自增函數(shù)incrementAndGet()的源碼時捣郊,發(fā)現(xiàn)自增函數(shù)底層調用的是unsafe.getAndAddInt()辽狈。但是由于JDK本身只有Unsafe.class,只通過class文件中的參數(shù)名呛牲,并不能很好的了解方法的作用刮萌,所以我們通過OpenJDK 8 來查看Unsafe的源碼:
根據(jù)OpenJDK 8的源碼我們可以看出,getAndAddInt()循環(huán)獲取給定對象o中的偏移量處的值v娘扩,然后判斷內存值是否等于v着茸。如果相等則將內存值設置為 v + delta,否則返回false琐旁,繼續(xù)循環(huán)進行重試涮阔,直到設置成功才能退出循環(huán),并且將舊值返回灰殴。整個“比較+更新”操作封裝在compareAndSwapInt()中敬特,在JNI里是借助于一個CPU指令完成的,屬于原子操作牺陶,可以保證多個線程都能夠看到同一個變量的修改值伟阔。
后續(xù)JDK通過CPU的cmpxchg指令,去比較寄存器中的 A 和 內存中的值 V掰伸。如果相等皱炉,就把要寫入的新值 B 存入內存中。如果不相等狮鸭,就將內存值 V 賦值給寄存器中的值 A合搅。然后通過Java代碼中的while循環(huán)再次調用cmpxchg指令進行重試,直到設置成功為止怕篷。
CAS雖然很高效,但是它也存在三大問題酗昼,這里也簡單說一下:
1. ABA問題廊谓。CAS需要在操作值的時候檢查內存值是否發(fā)生變化,沒有發(fā)生變化才會更新內存值麻削。但是如果內存值原來是A蒸痹,后來變成了B,然后又變成了A呛哟,那么CAS進行檢查時會發(fā)現(xiàn)值沒有發(fā)生變化叠荠,但是實際上是有變化的。ABA問題的解決思路就是在變量前面添加版本號扫责,每次變量更新的時候都把版本號加一榛鼎,這樣變化過程就從“A-B-A”變成了“1A-2B-3A”。
JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,具體操作封裝在compareAndSet()中者娱。compareAndSet()首先檢查當前引用和當前標志與預期引用和預期標志是否相等抡笼,如果都相等,則以原子方式將引用值和標志的值設置為給定的更新值黄鳍。
2.循環(huán)時間長開銷大推姻。CAS操作如果長時間不成功,會導致其一直自旋框沟,給CPU帶來非常大的開銷藏古。
3.只能保證一個共享變量的原子操作。對一個共享變量執(zhí)行操作時忍燥,CAS能夠保證原子操作拧晕,但是對多個共享變量操作時,CAS是無法保證操作的原子性的灾前。
Java從1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性防症,可以把多個變量放在一個對象里來進行CAS操作。
2.自旋鎖 VS 適應性自旋鎖
在介紹自旋鎖前哎甲,我們需要介紹一些前提知識來幫助大家明白自旋鎖的概念畅厢。
阻塞或喚醒一個Java線程需要操作系統(tǒng)切換CPU狀態(tài)來完成,這種狀態(tài)轉換需要耗費處理器時間票罐。如果同步代碼塊中的內容過于簡單庆聘,狀態(tài)轉換消耗的時間有可能比用戶代碼執(zhí)行的時間還要長。
在許多場景中吞加,同步資源的鎖定時間很短裙犹,為了這一小段時間去切換線程,線程掛起和恢復現(xiàn)場的花費可能會讓系統(tǒng)得不償失衔憨。如果物理機器有多個處理器叶圃,能夠讓兩個或以上的線程同時并行執(zhí)行,我們就可以讓后面那個請求鎖的線程不放棄CPU的執(zhí)行時間践图,看看持有鎖的線程是否很快就會釋放鎖掺冠。
而為了讓當前線程“稍等一下”,我們需讓當前線程進行自旋码党,如果在自旋完成后前面鎖定同步資源的線程已經釋放了鎖德崭,那么當前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷揖盘。這就是自旋鎖眉厨。
自旋鎖本身是有缺點的,它不能代替阻塞兽狭。自旋等待雖然避免了線程切換的開銷憾股,但它要占用處理器時間鹿蜀。如果鎖被占用的時間很短,自旋等待的效果就會非常好荔燎。反之耻姥,如果鎖被占用的時間很長,那么自旋的線程只會白浪費處理器資源有咨。所以琐簇,自旋等待的時間必須要有一定的限度,如果自旋超過了限定次數(shù)(默認是10次座享,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖婉商,就應當掛起線程。
自旋鎖的實現(xiàn)原理同樣也是CAS渣叛,AtomicInteger中調用unsafe進行自增操作的源碼中的do-while循環(huán)就是一個自旋操作丈秩,如果修改數(shù)值失敗則通過循環(huán)來執(zhí)行自旋,直至修改成功淳衙。
自旋鎖在JDK1.4.2中引入蘑秽,使用-XX:+UseSpinning來開啟。JDK 6中變?yōu)槟J開啟箫攀,并且引入了自適應的自旋鎖(適應性自旋鎖)肠牲。
自適應意味著自旋的時間(次數(shù))不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定靴跛。如果在同一個鎖對象上缀雳,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行中梢睛,那么虛擬機就會認為這次自旋也是很有可能再次成功肥印,進而它將允許自旋等待持續(xù)相對更長的時間。如果對于某個鎖绝葡,自旋很少成功獲得過深碱,那在以后嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程藏畅,避免浪費處理器資源敷硅。
在自旋鎖中另有三種常見的鎖形式:TicketLock、CLHlock和MCSlock墓赴,本文中僅做名詞介紹竞膳,不做深入講解航瞭,感興趣的同學可以自行查閱相關資料诫硕。
3.無鎖 VS 偏向鎖 VS 輕量級鎖 VS 重量級鎖
這四種鎖是指鎖的狀態(tài),專門針對synchronized的刊侯。在介紹這四種鎖狀態(tài)之前還需要介紹一些額外的知識章办。
首先為什么Synchronized能實現(xiàn)線程同步?
在回答這個問題之前我們需要了解兩個重要的概念:“Java對象頭”、“Monitor”藕届。
Java對象頭
synchronized是悲觀鎖挪蹭,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在Java對象頭里的休偶,而Java對象頭又是什么呢梁厉?
我們以Hotspot虛擬機為例,Hotspot的對象頭主要包括兩部分數(shù)據(jù):Mark Word(標記字段)踏兜、Klass Pointer(類型指針)词顾。
Mark Word:默認存儲對象的HashCode,分代年齡和鎖標志位信息碱妆。這些信息都是與對象自身定義無關的數(shù)據(jù)肉盹,所以Mark Word被設計成一個非固定的數(shù)據(jù)結構以便在極小的空間內存存儲盡量多的數(shù)據(jù)。它會根據(jù)對象的狀態(tài)復用自己的存儲空間疹尾,也就是說在運行期間Mark Word里存儲的數(shù)據(jù)會隨著鎖標志位的變化而變化上忍。
Klass Point:對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例纳本。
Monitor:可以理解為一個同步工具或一種同步機制窍蓝,通常被描述為一個對象。每一個Java對象就有一把看不見的鎖饮醇,稱為內部鎖或者Monitor鎖它抱。
Monitor是線程私有的數(shù)據(jù)結構,每一個線程都有一個可用monitor record列表朴艰,同時還有一個全局的可用列表观蓄。每一個被鎖住的對象都會和一個monitor關聯(lián),同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標識祠墅,表示該鎖被這個線程占用侮穿。
現(xiàn)在話題回到synchronized,synchronized通過Monitor來實現(xiàn)線程同步毁嗦,Monitor是依賴于底層的操作系統(tǒng)的Mutex Lock(互斥鎖)來實現(xiàn)的線程同步亲茅。
如同我們在自旋鎖中提到的“阻塞或喚醒一個Java線程需要操作系統(tǒng)切換CPU狀態(tài)來完成,這種狀態(tài)轉換需要耗費處理器時間狗准。如果同步代碼塊中的內容過于簡單克锣,狀態(tài)轉換消耗的時間有可能比用戶代碼執(zhí)行的時間還要長”。這種方式就是synchronized最初實現(xiàn)同步的方式腔长,這就是JDK 6之前synchronized效率低的原因袭祟。這種依賴于操作系統(tǒng)Mutex Lock所實現(xiàn)的鎖我們稱之為“重量級鎖”,JDK 6中為了減少獲得鎖和釋放鎖帶來的性能消耗捞附,引入了“偏向鎖”和“輕量級鎖”巾乳。
所以目前鎖一共有4種狀態(tài)您没,級別從低到高依次是:無鎖、偏向鎖胆绊、輕量級鎖和重量級鎖氨鹏。鎖狀態(tài)只能升級不能降級。
通過上面的介紹压状,我們對synchronized的加鎖機制以及相關知識有了一個了解仆抵,那么下面我們給出四種鎖狀態(tài)對應的的Mark Word內容,然后再分別講解四種鎖狀態(tài)的思路以及特點:
無鎖
無鎖沒有對資源進行鎖定种冬,所有的線程都能訪問并修改同一個資源肢础,但同時只有一個線程能修改成功。
無鎖的特點就是修改操作在循環(huán)內進行碌廓,線程會不斷的嘗試修改共享資源传轰。如果沒有沖突就修改成功并退出,否則就會繼續(xù)循環(huán)嘗試谷婆。如果有多個線程修改同一個值慨蛙,必定會有一個線程能修改成功,而其他修改失敗的線程會不斷重試直到修改成功纪挎。上面我們介紹的CAS原理及應用即是無鎖的實現(xiàn)期贫。無鎖無法全面代替有鎖,但無鎖在某些場合下的性能是非常高的异袄。
偏向鎖
偏向鎖是指一段同步代碼一直被一個線程所訪問通砍,那么該線程會自動獲取鎖,降低獲取鎖的代價烤蜕。
在大多數(shù)情況下封孙,鎖總是由同一線程多次獲得,不存在多線程競爭讽营,所以出現(xiàn)了偏向鎖虎忌。其目標就是在只有一個線程執(zhí)行同步代碼塊時能夠提高性能。
當一個線程訪問同步代碼塊并獲取鎖時橱鹏,會在Mark Word里存儲鎖偏向的線程ID膜蠢。在線程進入和退出同步塊時不再通過CAS操作來加鎖和解鎖,而是檢測Mark Word里是否存儲著指向當前線程的偏向鎖莉兰。引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑挑围,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令即可糖荒。
偏向鎖只有遇到其他線程嘗試競爭偏向鎖時杉辙,持有偏向鎖的線程才會釋放鎖,線程不會主動釋放偏向鎖寂嘉。偏向鎖的撤銷奏瞬,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),它會首先暫停擁有偏向鎖的線程泉孩,判斷鎖對象是否處于被鎖定狀態(tài)硼端。撤銷偏向鎖后恢復到無鎖(標志位為“01”)或輕量級鎖(標志位為“00”)的狀態(tài)。
偏向鎖在JDK 6及以后的JVM里是默認啟用的寓搬≌渥颍可以通過JVM參數(shù)關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之后程序默認會進入輕量級鎖狀態(tài)句喷。
輕量級鎖
是指當鎖是偏向鎖的時候镣典,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖唾琼,其他線程會通過自旋的形式嘗試獲取鎖兄春,不會阻塞,從而提高性能锡溯。
在代碼進入同步塊的時候赶舆,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標志位為“01”狀態(tài),是否為偏向鎖為“0”)祭饭,虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間芜茵,用于存儲鎖對象目前的Mark Word的拷貝,然后拷貝對象頭中的Mark Word復制到鎖記錄中倡蝙。
拷貝成功后九串,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock Record里的owner指針指向對象的Mark Word寺鸥。
如果這個更新動作成功了猪钮,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位設置為“00”胆建,表示此對象處于輕量級鎖定狀態(tài)躬贡。
如果輕量級鎖的更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀眼坏,如果是就說明當前線程已經擁有了這個對象的鎖拂玻,那就可以直接進入同步塊繼續(xù)執(zhí)行,否則說明多個線程競爭鎖宰译。
若當前只有一個等待線程檐蚜,則該線程通過自旋進行等待。但是當自旋超過一定的次數(shù)沿侈,或者一個線程在持有鎖闯第,一個在自旋,又有第三個來訪時缀拭,輕量級鎖升級為重量級鎖咳短。
重量級鎖
升級為重量級鎖時填帽,鎖標志的狀態(tài)值變?yōu)椤?0”,此時Mark Word中存儲的是指向重量級鎖的指針咙好,此時等待鎖的線程都會進入阻塞狀態(tài)篡腌。
整體的鎖狀態(tài)升級流程如下:
綜上,偏向鎖通過對比Mark Word解決加鎖問題勾效,避免執(zhí)行CAS操作嘹悼。而輕量級鎖是通過用CAS操作和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能层宫。重量級鎖是將除了擁有鎖的線程以外的線程都阻塞杨伙。
4.公平鎖 VS 非公平鎖
公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊萌腿,隊列中的第一個線程才能獲得鎖限匣。公平鎖的優(yōu)點是等待鎖的線程不會餓死。缺點是整體吞吐效率相對非公平鎖要低毁菱,等待隊列中除第一個線程以外的所有線程都會阻塞膛腐,CPU喚醒阻塞線程的開銷比非公平鎖大。
非公平鎖是多個線程加鎖時直接嘗試獲取鎖鼎俘,獲取不到才會到等待隊列的隊尾等待哲身。但如果此時鎖剛好可用,那么這個線程可以無需阻塞直接獲取到鎖贸伐,所以非公平鎖有可能出現(xiàn)后申請鎖的線程先獲取鎖的場景勘天。非公平鎖的優(yōu)點是可以減少喚起線程的開銷,整體的吞吐效率高捉邢,因為線程有幾率不阻塞直接獲得鎖脯丝,CPU不必喚醒所有線程。缺點是處于等待隊列中的線程可能會餓死伏伐,或者等很久才會獲得鎖宠进。
直接用語言描述可能有點抽象,這里作者用從別處看到的一個例子來講述一下公平鎖和非公平鎖藐翎。
如上圖所示材蹬,假設有一口水井,有管理員看守吝镣,管理員有一把鎖堤器,只有拿到鎖的人才能夠打水,打完水要把鎖還給管理員末贾。每個過來打水的人都要管理員的允許并拿到鎖之后才能去打水闸溃,如果前面有人正在打水,那么這個想要打水的人就必須排隊。管理員會查看下一個要去打水的人是不是隊伍里排最前面的人辉川,如果是的話表蝙,才會給你鎖讓你去打水;如果你不是排第一的人乓旗,就必須去隊尾排隊府蛇,這就是公平鎖。
但是對于非公平鎖寸齐,管理員對打水的人沒有要求。即使等待隊伍里有排隊等待的人抄谐,但如果在上一個人剛打完水把鎖還給管理員而且管理員還沒有允許等待隊伍里下一個人去打水時渺鹦,剛好來了一個插隊的人,這個插隊的人是可以直接從管理員那里拿到鎖去打水蛹含,不需要排隊毅厚,原本排隊等待的人只能繼續(xù)等待。如下圖所示:
接下來我們通過ReentrantLock的源碼來講解公平鎖和非公平鎖浦箱。
根據(jù)代碼可知吸耿,ReentrantLock里面有一個內部類Sync,Sync繼承AQS(AbstractQueuedSynchronizer)酷窥,添加鎖和釋放鎖的大部分操作實際上都是在Sync中實現(xiàn)的咽安。它有公平鎖FairSync和非公平鎖NonfairSync兩個子類。ReentrantLock默認使用非公平鎖蓬推,也可以通過構造器來顯示的指定使用公平鎖妆棒。
下面我們來看一下公平鎖與非公平鎖的加鎖方法的源碼:
通過上圖中的源代碼對比,我們可以明顯的看出公平鎖與非公平鎖的lock()方法唯一的區(qū)別就在于公平鎖在獲取同步狀態(tài)時多了一個限制條件:hasQueuedPredecessors()沸伏。
再進入hasQueuedPredecessors()糕珊,可以看到該方法主要做一件事情:主要是判斷當前線程是否位于同步隊列中的第一個。如果是則返回true毅糟,否則返回false红选。
綜上,公平鎖就是通過同步隊列來實現(xiàn)多個線程按照申請鎖的順序來獲取鎖姆另,從而實現(xiàn)公平的特性喇肋。非公平鎖加鎖時不考慮排隊等待問題,直接嘗試獲取鎖迹辐,所以存在后申請卻先獲得鎖的情況苟蹈。
5.可重入鎖 VS 非可重入鎖
可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候右核,再進入該線程的內層方法會自動獲取鎖(前提鎖對象得是同一個對象或者class)慧脱,不會因為之前已經獲取過還沒釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖贺喝,可重入鎖的一個優(yōu)點是可一定程度避免死鎖菱鸥。下面用示例代碼來進行分析:
在上面的代碼中宗兼,類中的兩個方法都是被內置鎖synchronized修飾的,doSomething()方法中調用doOthers()方法氮采。因為內置鎖是可重入的殷绍,所以同一個線程在調用doOthers()時可以直接獲得當前對象的鎖,進入doOthers()進行操作鹊漠。
如果是一個不可重入鎖主到,那么當前線程在調用doOthers()之前需要將執(zhí)行doSomething()時獲取當前對象的鎖釋放掉,實際上該對象鎖已被當前線程所持有躯概,且無法釋放登钥。所以此時會出現(xiàn)死鎖。
而為什么可重入鎖就可以在嵌套調用時可以自動獲得鎖呢娶靡?我們通過圖示和源碼來分別解析一下牧牢。
還是打水的例子,有多個人在排隊打水姿锭,此時管理員允許鎖和同一個人的多個水桶綁定塔鳍。這個人用多個水桶打水時,第一個水桶和鎖綁定并打完水之后呻此,第二個水桶也可以直接和鎖綁定并開始打水轮纫,所有的水桶都打完水之后打水人才會將鎖還給管理員。這個人的所有打水流程都能夠成功執(zhí)行焚鲜,后續(xù)等待的人也能夠打到水蜡感。這就是可重入鎖。
但如果是非可重入鎖的話恃泪,此時管理員只允許鎖和同一個人的一個水桶綁定郑兴。第一個水桶和鎖綁定打完水之后并不會釋放鎖,導致第二個水桶不能和鎖綁定也無法打水贝乎。當前線程出現(xiàn)死鎖情连,整個等待隊列中的所有線程都無法被喚醒。
之前我們說過ReentrantLock和synchronized都是重入鎖览效,那么我們通過重入鎖ReentrantLock以及非可重入鎖NonReentrantLock的源碼來對比分析一下為什么非可重入鎖在重復調用同步資源時會出現(xiàn)死鎖却舀。
首先ReentrantLock和NonReentrantLock都繼承父類AQS,其父類AQS中維護了一個同步狀態(tài)status來計數(shù)重入次數(shù)锤灿,status初始值為0挽拔。
當線程嘗試獲取鎖時,可重入鎖先嘗試獲取并更新status值但校,如果status == 0表示沒有其他線程在執(zhí)行同步代碼螃诅,則把status置為1,當前線程開始執(zhí)行。如果status != 0术裸,則判斷當前線程是否是獲取到這個鎖的線程倘是,如果是的話執(zhí)行status+1,且當前線程可以再次獲取鎖袭艺。而非可重入鎖是直接去獲取并嘗試更新當前status的值搀崭,如果status != 0的話會導致其獲取鎖失敗,當前線程阻塞猾编。
釋放鎖時瘤睹,可重入鎖同樣先獲取當前status的值,在當前線程是持有鎖的線程的前提下答倡。如果status-1 == 0轰传,則表示當前線程所有重復獲取鎖的操作都已經執(zhí)行完畢,然后該線程才會真正釋放鎖苇羡。而非可重入鎖則是在確定當前線程是持有鎖的線程之后绸吸,直接將status置為0鼻弧,將鎖釋放设江。
6.獨享鎖 VS 共享鎖
獨享鎖和共享鎖同樣是一種概念。我們先介紹一下具體的概念攘轩,然后通過ReentrantLock和ReentrantReadWriteLock的源碼來介紹獨享鎖和共享鎖叉存。
獨享鎖也叫排他鎖,是指該鎖一次只能被一個線程所持有度帮。如果線程T對數(shù)據(jù)A加上排它鎖后歼捏,則其他線程不能再對A加任何類型的鎖。獲得排它鎖的線程即能讀數(shù)據(jù)又能修改數(shù)據(jù)笨篷。JDK中的synchronized和JUC中Lock的實現(xiàn)類就是互斥鎖瞳秽。
共享鎖是指該鎖可被多個線程所持有。如果線程T對數(shù)據(jù)A加上共享鎖后率翅,則其他線程只能對A再加共享鎖练俐,不能加排它鎖。獲得共享鎖的線程只能讀數(shù)據(jù)冕臭,不能修改數(shù)據(jù)腺晾。
獨享鎖與共享鎖也是通過AQS來實現(xiàn)的,通過實現(xiàn)不同的方法辜贵,來實現(xiàn)獨享或者共享悯蝉。
下圖為ReentrantReadWriteLock的部分源碼:
我們看到ReentrantReadWriteLock有兩把鎖:ReadLock和WriteLock,由詞知意托慨,一個讀鎖一個寫鎖鼻由,合稱“讀寫鎖”。再進一步觀察可以發(fā)現(xiàn)ReadLock和WriteLock是靠內部類Sync實現(xiàn)的鎖。Sync是AQS的一個子類嗡靡,這種結構在CountDownLatch跺撼、ReentrantLock、Semaphore里面也都存在讨彼。
在ReentrantReadWriteLock里面歉井,讀鎖和寫鎖的鎖主體都是Sync,但讀鎖和寫鎖的加鎖方式不一樣哈误。讀鎖是共享鎖哩至,寫鎖是獨享鎖。讀鎖的共享鎖可保證并發(fā)讀非常高效蜜自,而讀寫菩貌、寫讀、寫寫的過程互斥重荠,因為讀鎖和寫鎖是分離的箭阶。所以ReentrantReadWriteLock的并發(fā)性相比一般的互斥鎖有了很大提升。
那讀鎖和寫鎖的具體加鎖方式有什么區(qū)別呢戈鲁?在了解源碼之前我們需要回顧一下其他知識仇参。
在最開始提及AQS的時候我們也提到了state字段(int類型,32位)婆殿,該字段用來描述有多少線程獲持有鎖诈乒。
在獨享鎖中這個值通常是0或者1(如果是重入鎖的話state值就是重入的次數(shù)),在共享鎖中state就是持有鎖的數(shù)量婆芦。但是在ReentrantReadWriteLock中有讀怕磨、寫兩把鎖,所以需要在一個整型變量state上分別描述讀鎖和寫鎖的數(shù)量(或者也可以叫狀態(tài))消约。于是將state變量“按位切割”切分成了兩個部分肠鲫,高16位表示讀鎖狀態(tài)(讀鎖個數(shù)),低16位表示寫鎖狀態(tài)(寫鎖個數(shù))或粮。如下圖所示:
了解了概念之后我們再來看代碼导饲,先看寫鎖的加鎖源碼:
這段代碼首先取到當前鎖的個數(shù)c,然后再通過c來獲取寫鎖的個數(shù)w被啼。因為寫鎖是低16位帜消,所以取低16位的最大值與當前的c做與運算( int w = exclusiveCount(c); ),高16位和0與運算后是0浓体,剩下的就是低位運算的值泡挺,同時也是持有寫鎖的線程數(shù)目。
在取到寫鎖線程的數(shù)目后命浴,首先判斷是否已經有線程持有了鎖娄猫。如果已經有線程持有了鎖(c!=0)贱除,則查看當前寫鎖線程的數(shù)目,如果寫線程數(shù)為0(即此時存在讀鎖)或者持有鎖的線程不是當前線程就返回失斚蹦纭(涉及到公平鎖和非公平鎖的實現(xiàn))月幌。
如果寫入鎖的數(shù)量大于最大數(shù)(65535,2的16次方-1)就拋出一個Error悬蔽。
如果當且寫線程數(shù)為0(那么讀線程也應該為0扯躺,因為上面已經處理c!=0的情況),并且當前線程需要阻塞那么就返回失斝А录语;如果通過CAS增加寫線程數(shù)失敗也返回失敗。
如果c=0禾乘,w=0或者c>0澎埠,w>0(重入),則設置當前線程或鎖的擁有者始藕,返回成功蒲稳!
tryAcquire()除了重入條件(當前線程為獲取了寫鎖的線程)之外,增加了一個讀鎖是否存在的判斷伍派。如果存在讀鎖江耀,則寫鎖不能被獲取,原因在于:必須確保寫鎖的操作對讀鎖可見拙已,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取决记,那么正在運行的其他讀線程就無法感知到當前寫線程的操作摧冀。
因此倍踪,只有等待其他讀線程都釋放了讀鎖,寫鎖才能被當前線程獲取索昂,而寫鎖一旦被獲取建车,則其他讀寫線程的后續(xù)訪問均被阻塞。寫鎖的釋放與ReentrantLock的釋放過程基本類似椒惨,每次釋放均減少寫狀態(tài)缤至,當寫狀態(tài)為0時表示寫鎖已被釋放,然后等待的讀寫線程才能夠繼續(xù)訪問讀寫鎖康谆,同時前次寫線程的修改對后續(xù)的讀寫線程可見领斥。
接著是讀鎖的代碼:
可以看到在tryAcquireShared(int unused)方法中,如果其他線程已經獲取了寫鎖沃暗,則當前線程獲取讀鎖失敗月洛,進入等待狀態(tài)。如果當前線程獲取了寫鎖或者寫鎖未被獲取孽锥,則當前線程(線程安全嚼黔,依靠CAS保證)增加讀狀態(tài)细层,成功獲取讀鎖。讀鎖的每次釋放(線程安全的唬涧,可能有多個讀線程同時釋放讀鎖)均減少讀狀態(tài)疫赎,減少的值是“1<<16”。所以讀寫鎖才能實現(xiàn)讀讀的過程共享碎节,而讀寫捧搞、寫讀、寫寫的過程互斥狮荔。
此時实牡,我們再回頭看一下互斥鎖ReentrantLock中公平鎖和非公平鎖的加鎖源碼:
我們發(fā)現(xiàn)在ReentrantLock雖然有公平鎖和非公平鎖兩種,但是它們添加的都是獨享鎖轴合。根據(jù)源碼所示创坞,當某一個線程調用lock方法獲取鎖時,如果同步資源沒有被其他線程鎖住受葛,那么當前線程在使用CAS更新state成功后就會成功搶占該資源题涨。而如果公共資源被占用且不是被當前線程占用,那么就會加鎖失敗总滩。所以可以確定ReentrantLock無論讀操作還是寫操作纲堵,添加的鎖都是都是獨享鎖。
結語
本文Java中常用的鎖以及常見的鎖的概念進行了基本介紹闰渔,并從源碼以及實際應用的角度進行了對比分析席函。限于篇幅以及個人水平,沒有在本篇文章中對所有內容進行深層次的講解冈涧。
其實Java本身已經對鎖本身進行了良好的封裝茂附,降低了研發(fā)同學在平時工作中的使用難度。但是研發(fā)同學也需要熟悉鎖的底層原理督弓,不同場景下選擇最適合的鎖营曼。而且源碼中的思路都是非常好的思路,也是值得大家去學習和借鑒的愚隧。
專注于Java架構師技術分享蒂阱,撩我免費送Java全套架構師晉級資料
(Java架構師交流企*-**-*鵝*-*裙*-*:445*-**-*820*-**-*908)