Java 鎖相關(guān)

1、概述

在并發(fā)編程中,經(jīng)常遇到多個線程訪問同一個共享資源 ,這時候作為開發(fā)者必須考慮如何維護數(shù)據(jù)一致性,在java中synchronized關(guān)鍵字被常用于維護數(shù)據(jù)一致性。synchronized機制是給共享資源上鎖,只有拿到鎖的線程才可以訪問共享資源,這樣就可以強制使得對共享資源的訪問都是順序的。一般在java中所說的鎖就是指的內(nèi)置鎖,每個java對象都可以作為一個實現(xiàn)同步的鎖,雖然說在java中一切皆對象, 但是鎖必須是引用類型的,基本數(shù)據(jù)類型則不可以 。每一個引用類型的對象都可以隱式的扮演一個用于同步的鎖的角色,執(zhí)行線程進入synchronized塊之前會自動獲得鎖,無論是通過正常語句退出還是執(zhí)行過程中拋出了異常,線程都會在放棄對synchronized塊的控制時自動釋放鎖。 獲得鎖的唯一途徑就是進入這個內(nèi)部鎖保護的同步塊或方法 。當(dāng)多個線程對共享資源訪問的時候,只能有一個線程可以獲得該共享資源的鎖,當(dāng)線程A嘗試獲取目前在線程B手上的鎖的時候,線程A必須等待或者阻塞,直到線程B釋放該鎖為止,否則線程A將一直等待下去,因此java內(nèi)置鎖也稱作互斥鎖,也即是說鎖實際上是一種互斥機制。根據(jù)使用方式的不同一般我們會將鎖分為對象鎖和類鎖,兩個鎖是有很大差別的,對象鎖是作用在實例方法或者一個對象實例上面的,而類鎖是作用在靜態(tài)方法或者Class對象上面的。一個類可以有多個實例對象,因此一個類的對象鎖可能會有多個,但是每個類只有一個Class對象,所以類鎖只有一個。

2、鎖相關(guān)概念

從不同角度可以將鎖進行分類,一種具體的鎖操作,在不同分類方式下一般會有對應(yīng)的所屬。

2.1 樂觀鎖和悲觀鎖

樂觀鎖是正如其名字,很樂觀,默認覺得不會有人在其進行操作的時候進行修改操作,遇到并發(fā)寫的可能性低,即認為讀多寫少。所以不會上鎖,但是在更新的時候會判斷一下在此期間數(shù)據(jù)有么有被其他人更新,方法可以是在寫時先讀出當(dāng)前版本號,然后加鎖操作,再比較跟上一次的版本號,如果沒變則更新。如果版本號變了,說明在此期間有人修改了數(shù)據(jù),則要重復(fù)讀-比較-寫的操作。

悲觀鎖是就是就不一樣了,它總認為有人要和它爭搶操作的數(shù)據(jù),所以每次在讀寫數(shù)據(jù)的時候都會上鎖,這樣別人想讀寫這個數(shù)據(jù)就會被阻塞直到拿到鎖。

2.2 公平鎖和非公平鎖

公平鎖是指多個線程按照申請鎖的順序來獲取鎖,先到先得。

非公平鎖是指多個線程獲取鎖的順序并不是按照申請鎖的順序,有可能后申請的線程比先申請的線程優(yōu)先獲取鎖。這就可能,有的申請者老是被插隊而造成優(yōu)先級反轉(zhuǎn)或者饑餓現(xiàn)象。

2.3 可重入鎖和不可重入鎖

可重入鎖又名遞歸鎖,是指當(dāng)一個線程已經(jīng)擁有某一對象鎖時候,就可以遞歸的調(diào)用該對象的帶鎖方法。比如說線程調(diào)用對象A的帶鎖方法a,而方法a中要調(diào)用這個對象A的帶鎖方法b,這時如果這時可重入鎖,線程由于已經(jīng)在調(diào)用a方法時獲得了對象A的鎖,所以調(diào)用b方法時也自然可以進入到b方法中,而不需要這個線程釋放調(diào)用a方法時候的鎖,再重新在調(diào)用b方法時候獲取鎖。

不可重入鎖恰好相反。所以在上述情況下,就會出現(xiàn)問題,即要調(diào)用b方法就要釋放a方法時候獲取的鎖再重新獲取對象A的鎖,而此時a方法還沒有執(zhí)行完畢,故a方法的鎖不能釋放。這就出現(xiàn)了死鎖的情況。

所以一般來說Java里面的鎖設(shè)計都是可重入鎖。一個線程一旦獲得了對象的鎖,就可以調(diào)用這個對象的所有方法而不需要重新獲取這個對象的鎖。

2.4 獨享鎖和共享鎖

獨享鎖是指該鎖一次只能被一個線程所持有。共享鎖是指該鎖可被多個線程所持有。一般來說讀鎖是共享鎖,而寫鎖是獨享鎖。因為讀的時候不會改變內(nèi)容,所以多個讀線程可以同時讀取一個對象。但是一般來說這時候就不能讓寫線程進來進行操作了。而寫線程如果好幾個線程同時寫一個對象,那就會造成混亂,所以寫操作時候一般都是獨享對象的。同時也可以理解,共享鎖的執(zhí)行效率是要高于獨享鎖的。

2.5 分段鎖

分段鎖是一種鎖的設(shè)計思想,它的設(shè)計目的是細化鎖的粒度,當(dāng)操作不需要更新整個數(shù)組的時候,就僅僅針對數(shù)組中的一項進行加鎖操作。比如本來要鎖整個數(shù)據(jù)庫的,改成鎖其中一張表,這樣的好處就是對于同一個數(shù)據(jù)庫但不是同一張表的操作可以同時進行,而不用一個等另一個操作完。ConcurrentHashMap內(nèi)部也是通過分段鎖的形式來實現(xiàn)高效的并發(fā)操作。ConcurrentHashMap中的分段鎖稱為Segment,它即類似于HashMap的結(jié)構(gòu),即內(nèi)部擁有一個Entry數(shù)組,數(shù)組中的每個元素又是一個鏈表。當(dāng)需要put元素的時候,并不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然后對這個分段進行加鎖,所以當(dāng)多線程put的時候,只要不是放在一個分段中,就實現(xiàn)了并行的插入。這么做的缺點是,在統(tǒng)計size的時候,需要獲取hashmap全局信息,這時就需要獲取所有的分段鎖才能統(tǒng)計。

2.6 自旋鎖

自旋鎖是指Java一種等待獲取鎖的策略。自旋鎖原理是如果持有鎖的線程能在很短時間內(nèi)釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進入阻塞掛起狀態(tài),它們只需要等一等(自旋),等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內(nèi)核的切換的消耗,可以簡單理解為不停的while循環(huán)去獲取鎖,沒有獲取到就一直循環(huán),獲取到了就調(diào)出循環(huán)進行下一步操作。所以說線程自旋是需要消耗cup的,說白了就是讓cup在做無用功。所以一般來說自旋鎖操作出現(xiàn)在估計能馬上獲取到鎖的情況下進行。自旋了一段時間如果還沒有拿到鎖,一般來說還是要取消自選進入阻塞掛起狀態(tài)。這個零界點就是切換用戶內(nèi)核線程的消耗和自旋對CPU的消耗那個損失更大。

3、synchronized

前面介紹了鎖在不同角度下的分類,現(xiàn)在討論一下線程同步中很重要的synchronized。先將它實現(xiàn)的鎖進行一個歸類,synchronized操作內(nèi)部的鎖是:悲觀鎖、非公平鎖、可重入鎖、獨享鎖、就對象而言不是分段鎖、內(nèi)部策略中存在自旋鎖。接下來詳細說下synchronized的鎖機制。

任何一個對象都有一個Monitor與之關(guān)聯(lián),當(dāng)一個Monitor被某一線程持有后,它將處于鎖定狀態(tài)。Synchronized在JVM里的實現(xiàn)都是基于進入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步,雖然具體實現(xiàn)細節(jié)不一樣,但是都可以通過成對的MonitorEnter和MonitorExit指令來實現(xiàn)。MonitorEnter指令插入在同步代碼塊的開始位置,當(dāng)代碼執(zhí)行到該指令時,將會嘗試獲取該對象Monitor的所有權(quán),即嘗試獲得該對象的鎖,而monitorExit指令則插入在方法結(jié)束處和異常處,JVM保證每個MonitorEnter必須有對應(yīng)的MonitorExit。

3.1 Java對象頭

synchronized使用的鎖是存放在Java對象頭里面,具體位置是對象頭里面的MarkWord。MarkWord是java對象數(shù)據(jù)結(jié)構(gòu)中的一部分。markword數(shù)據(jù)的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32bit和64bit,它的最后2bit是鎖狀態(tài)標(biāo)志位,用來標(biāo)記當(dāng)前對象的狀態(tài),對象的所處的狀態(tài),決定了markword存儲的內(nèi)容。MarkWord里默認數(shù)據(jù)是存儲對象的HashCode等信息,但是會隨著對象的運行改變而發(fā)生變化,不同的鎖狀態(tài)對應(yīng)著不同的記錄存儲方式,如下表所示:

狀態(tài) 標(biāo)志位 存儲內(nèi)容
未鎖定 01 對象哈希碼、對象分代年齡、偏向鎖標(biāo)志位
偏向鎖 01 偏向線程ID、偏向時間戳、對象分代年齡、偏向鎖標(biāo)志位
輕量級鎖定 00 指向鎖記錄的指針
重量級鎖定) 10 執(zhí)行重量級鎖定的指針
GC標(biāo)記 11 空(不需要記錄信息)

3.2 Monitor Record

Monitor Record是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每一個被鎖住的對象都會和一個monitor record關(guān)聯(lián)(對象頭的MarkWord中的LockWord指向monitor record的起始地址),同時monitor record中有一個Owner字段存放擁有該鎖的線程的唯一標(biāo)識,表示該鎖被這個線程占用。

3.3 CAS

CAS,compare and swap的縮寫,中文翻譯成比較并交換。在java語言之前,并發(fā)就已經(jīng)廣泛存在并在服務(wù)器領(lǐng)域得到了大量的應(yīng)用。所以硬件廠商老早就在芯片中加入了大量處理并發(fā)操作的原語,從而在硬件層面提升效率。在intel的CPU中,使用cmpxchg指令。在Java發(fā)展初期,java語言是不能夠利用硬件提供的這些便利來提升系統(tǒng)的性能的。而隨著java不斷的發(fā)展,Java本地方法(JNI)的出現(xiàn),使得java程序越過JVM直接調(diào)用本地方法提供了一種便捷的方式,因而java在并發(fā)的手段上也多了起來。而在Doug Lea提供的cucurenct包中,CAS理論是它實現(xiàn)整個java包的基石。

CAS 操作包含三個操作數(shù) —— 內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)。 如果內(nèi)存位置的值與預(yù)期原值相匹配,那么處理器會自動將該位置值更新為新值 。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當(dāng)前值。)CAS 有效地說明了“我認為位置 V 應(yīng)該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現(xiàn)在的值即可。”通常將 CAS 用于同步的方式是從地址 V 讀取值 A,執(zhí)行多步計算來獲得新 值 B,然后使用 CAS 將 V 的值從 A 改為 B。如果 V 處的值尚未同時更改,則 CAS 操作成功。類似于 CAS 的指令允許算法執(zhí)行讀-修改-寫操作,而無需害怕其他線程同時 修改變量,因為如果其他線程修改變量,那么 CAS 會檢測它(并失?。惴?可以對該操作重新計算。

3.4 鎖的不同狀態(tài)

對于synchronized,鎖一共有四種狀態(tài),無鎖狀態(tài),偏向鎖狀態(tài),輕量級鎖狀態(tài)和重量級鎖狀態(tài),它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,目的是為了提高獲得鎖和釋放鎖的效率。

3.4.1 偏向鎖

大多數(shù)情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖,減少不必要的CAS操作。當(dāng)一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要花費CAS操作來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖,如果測試成功,表示線程已經(jīng)獲得了鎖,如果測試失敗,則需要再測試下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖),如果沒有設(shè)置,則使用CAS競爭鎖,如果設(shè)置了,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程(此時會引發(fā)競爭,偏向鎖會升級為輕量級鎖)。如果當(dāng)前線程執(zhí)行CAS獲取偏向鎖失敗(這一步是偏向鎖的關(guān)鍵),表示在該鎖對象上存在競爭并且這個時候另外一個線程獲得偏向鎖所有權(quán)。當(dāng)那個競爭失敗的線程運行到全局安全點(safepoint)時會讓持有偏向鎖的線程暫停,并讓持有偏向鎖的線程的私有Monitor Record列表中獲取一個空閑的記錄,將對象設(shè)置為LightWeight Lock(輕量級鎖)狀態(tài)并且Mark Word中的LockRecord指向剛才持有偏向鎖線程的Monitor record,最后在安全點暫停的競爭失敗的線程進入競爭輕量級鎖的路徑中,同時之前持有偏向鎖的被暫停的線程恢復(fù),繼續(xù)向下執(zhí)行代碼。

偏向鎖的獲得和撤銷

3.4.2 輕量級鎖和重量級鎖

輕量級鎖實現(xiàn)的背后基于這樣一種假設(shè),即在真實的情況下程序中的大部分同步代碼一般都處于無鎖競爭狀態(tài)(即單線程執(zhí)行環(huán)境),在無鎖競爭的情況下完全可以避免調(diào)用操作系統(tǒng)層面的重量級互斥鎖,取而代之的是在monitorenter和monitorexit中只需要依靠一條CAS原子指令就可以完成鎖的獲取及釋放。當(dāng)存在鎖競爭的情況下,執(zhí)行CAS指令失敗的線程將調(diào)用操作系統(tǒng)互斥鎖進入到阻塞狀態(tài),當(dāng)鎖被釋放的時候被喚醒。也就是說其實輕量級鎖是一把自旋鎖。整個流程如下:

線程在執(zhí)行同步塊之前,JVM會先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋操作來獲取鎖。當(dāng)沒有獲取到鎖的線程自旋到一定程度,還沒有獲取到鎖,說明這個對象不能用輕量級鎖來處理了,這時候就會將鎖升級為重量級鎖。沒有獲取到鎖的線程會被阻塞。一旦鎖升級成重量級鎖,就不會再恢復(fù)到輕量級鎖狀態(tài)。當(dāng)鎖處于這個狀態(tài)下,其他線程試圖獲取鎖時,都會被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。

輕量級鎖及其膨脹為重量級鎖

3.4.3 不同鎖狀態(tài)比較

鎖狀態(tài) 優(yōu)點 缺點 適用場景
偏向鎖 枷鎖解鎖沒有額外消耗,和執(zhí)行非同步快的速度近乎一樣 如果線程間存在 競爭,會有額外的撤銷消耗 基本上不存在多個線程訪問同步快的場景
輕量級鎖 基于自旋鎖, 競爭的線程不會阻塞減少了用戶線程與核心線程間切換的消耗 自旋操作會消耗CPU 同步快處理速度快,能馬上處理完畢讓出鎖的場景
重量級鎖 線程競爭不消耗CPU 線程阻塞,響應(yīng)時間慢 同步代碼塊執(zhí)行慢的場景

4、死鎖

死鎖是指兩個或兩個以上的線程或進程在執(zhí)行過程中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無外力作用,它們都將無法推進下去。此時稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖,這些永遠在互相等待的進程稱為死鎖進程。死鎖產(chǎn)生必須滿足如下四個條件:

① 互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內(nèi)某資源只由一個進程占用。如果此時還有其它進程請求資源,則請求者只能等待,直至占有資源的進程用畢釋放。

② 請求和保持條件:指進程已經(jīng)保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程占有,此時請求進程阻塞,但又對自己已獲得的其它資源保持不放。

③ 不剝奪條件:指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。

④ 循環(huán)等待:指在發(fā)生死鎖時,必然存在一個進程——資源的環(huán)形鏈,即進程集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1占用的資源;P1正在等待P2占用的資源,……,Pn正在等待已被P0占用的資源。

而解除死鎖的辦法就是打破上述四個條件的任意一個或幾個條件。

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

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

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