java中的CAS操作以及锁机制详解
關(guān)于CAS操作
CAS:Compare?And?Swap? ?--? 樂觀鎖策略
CAS(無鎖操作):使用CAS叫做比較交換來判斷是否出現(xiàn)沖突,出現(xiàn)沖突就重試當(dāng)前操作直到不沖突為止。
悲觀鎖(JDK1.6之前的內(nèi)建鎖):假設(shè)每一次執(zhí)行同步代碼塊均會(huì)產(chǎn)生沖突,所以當(dāng)線程獲取鎖成功,會(huì)阻塞其他嘗試獲取該鎖的線程。
樂觀鎖(Lock機(jī)制):假設(shè)所有線程訪問共享資源時(shí)不會(huì)出現(xiàn)沖突,既然不會(huì)出現(xiàn)沖突自然就不會(huì)阻塞其他線程。線程不會(huì)出現(xiàn)阻塞狀態(tài)。
1.1 CAS的操作過程
CAS的操作過程和(V,O,N)三個(gè)值有關(guān)
| ? | ? ? ? ? ? ? ? V | ? ? ? ? ? ? ? O | ? ? ? ? ? ? ? ?N |
| 具體含義 | 內(nèi)存中地址存放的實(shí)際值 | 預(yù)期值(舊值) | 更新后的值 |
下面就介紹一下操作的具體過程:
CAS在最開始的時(shí)候V和O的是相等的,N中的每次線程要進(jìn)行CAS操作時(shí)要新放入的值。當(dāng)要進(jìn)行CAS操作時(shí),要先判斷一下V和O,若相等,說明沒有V中的值還沒有被其他線程更改,這時(shí)就可以將N中的值替換到V中。若不相等表明N中的值已經(jīng)被其他的線程所更改,這時(shí)直接將N中的值返回即可;
當(dāng)多個(gè)線程同時(shí)進(jìn)行CAS操作時(shí),只有一個(gè)線程會(huì)成功,并且更新V的值,其余的線程會(huì)失敗。失敗后可以選擇不斷的進(jìn)行CAS操作,也可以直接掛起進(jìn)行等待;
?
CAS操作與內(nèi)建鎖的對(duì)比:
| 元老級(jí)的內(nèi)建鎖(synchronized) | 當(dāng)存在線程競(jìng)爭(zhēng)的情況下會(huì)出現(xiàn)線程阻塞以及喚醒帶來的性能問題,對(duì)應(yīng)互斥同步(阻塞同步),效率很低。 |
| CAS操作 | 并不會(huì)直接掛起線程,會(huì)嘗試若干次CAS操作,并非進(jìn)行耗時(shí)的掛起與喚醒操作,因此非阻塞式同步。 |
1.2 引入CAS所造成的問題
1.2.1 ?關(guān)于ABA問題,如下圖所示
解決思路:沿用數(shù)據(jù)庫的樂觀鎖機(jī)制,添加版本號(hào) 1A-2B-3A
JDK1.5提供atomic包下AtomicStampedReference類來解決CAS的ABA問題
1.2.2 自旋(CAS)會(huì)浪費(fèi)大量的處理器資源(CPU) 簡(jiǎn)單來說就是太耗費(fèi)時(shí)間
自旋與阻塞:舉個(gè)栗子來說,當(dāng)你開車到了一個(gè)十字路口時(shí),這時(shí)發(fā)現(xiàn)亮的是紅燈,那么這時(shí)的你就有兩種選擇,要么將車子直接熄火等待,要么踩住剎車等待;而這時(shí)的熄火和剎車就相當(dāng)于阻塞和自旋,那么我們?cè)撊绾稳ミx擇使用哪種處理方法呢?當(dāng)又回到剛才的栗子,當(dāng)你到達(dá)十字路口時(shí)發(fā)現(xiàn),額的神呀,今天的紅燈的等待時(shí)間竟然有半個(gè)小時(shí),這時(shí)你二話不說將車子熄火,自己蒙頭大睡來等待紅燈;但是當(dāng)你發(fā)現(xiàn)紅燈只有10秒鐘時(shí),你就會(huì)選擇踩住剎車來等待紅燈;你這里的處理機(jī)制就是在不同的情況下,哪種方法使得車子耗油最少就選擇哪個(gè)方法。又回到主題上,所以不能說自旋就一定會(huì)比阻塞的性能好。
為了解決這個(gè)問題,CPU就采取了一種處理機(jī)制:自適應(yīng)自旋(根據(jù)以往自旋等待時(shí)能否獲取到鎖,來動(dòng)態(tài)調(diào)整自旋的時(shí)間(循環(huán)嘗試數(shù)量))
自適應(yīng)自旋和出現(xiàn)其實(shí)也是與現(xiàn)實(shí)生活相關(guān)的,再次回到上個(gè)栗子中,如果之前不熄火等到了綠燈,那么這次不熄火的時(shí)間就長(zhǎng)一點(diǎn),多等會(huì)也沒事;如果之前不熄火沒等待綠燈, 那么這次不熄火的時(shí)間就短一點(diǎn)。自適應(yīng)自旋也是如此,如果在上一次自旋時(shí)獲取到鎖,則此次自旋時(shí)間稍微變長(zhǎng)一點(diǎn);如果在上一次自旋結(jié)束還沒有獲取到鎖,此次自旋時(shí)間稍微短一點(diǎn)。
1.2.3 關(guān)于公平性
公平性就可以這樣理解:
| 公平模式 | 比如一個(gè)鎖被很多線程等待是時(shí),鎖會(huì)選擇等待時(shí)間最長(zhǎng)的線程訪問它的臨界資源,可以和隊(duì)列類比一下理解為先到先得原則(lock鎖)它就是公平的。 |
| 非公平模式 | 而當(dāng)一個(gè)鎖是可以被后來的線程搶占時(shí),它就是非公平性的,比如內(nèi)建鎖(饑餓問題:由于訪問權(quán)限總是分配給了其他線程,而造成一個(gè)或多個(gè)線程被餓死的現(xiàn)象)。 |
自旋也是一種不公平的模式:處于阻塞狀態(tài)的線程無法立刻競(jìng)爭(zhēng)被釋放的鎖;而處于自旋狀態(tài)的線程很有可能先獲取到鎖。
2.Java對(duì)象頭(包括標(biāo)記字段和類型指針)
2.1.1 關(guān)于Mark Word
Mark Word(標(biāo)記字段):用于存儲(chǔ)運(yùn)行時(shí)對(duì)象自身的數(shù)據(jù),相當(dāng)于線程在運(yùn)行時(shí)貼上自己的標(biāo)簽,別的線程不可用,其占用內(nèi)存大小與虛擬機(jī)位長(zhǎng)一致,在運(yùn)行期間,考慮到JVM的空間效率,Mark Word被設(shè)計(jì)成為一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu),以便存儲(chǔ)更多有效的數(shù)據(jù)。
2.1.2 存儲(chǔ)運(yùn)行時(shí)對(duì)象自身數(shù)據(jù)(重量級(jí)鎖JDK1.6之前)
關(guān)于Epoch:Epoch?默認(rèn)最大值為40,到超過40后會(huì)變成輕量級(jí)鎖
2.1.3 關(guān)于鎖的升級(jí)
為了提高獲得鎖與釋放鎖的效率,JDK1.6之后對(duì)內(nèi)建鎖做了優(yōu)化(新增偏向,輕量級(jí)鎖),所以在 Java SE1.6 里鎖一共有四種狀態(tài),無鎖狀態(tài),偏向鎖狀態(tài),輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài),它會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí),鎖可以升級(jí)不能降級(jí),意味著偏向鎖升級(jí)成輕量級(jí)鎖后不能降級(jí)成偏向鎖。這種鎖升級(jí)卻不能降級(jí)的策略,目的是為了提高獲得鎖和釋放鎖的效率
?? ? ? ??
2.2偏向鎖
偏向鎖:最樂觀的鎖,從始至終只有一個(gè)線程請(qǐng)求一把鎖。
偏向鎖獲取,如下圖:
?? ? ? ? ? ? ? ? ? ? ? ??
當(dāng)一個(gè)線程訪問同步代碼塊并獲取鎖時(shí),會(huì)在對(duì)象頭的棧幀中的鎖記錄中記錄存儲(chǔ)偏向鎖的線程ID。以后該線程再次進(jìn)入同步塊時(shí)不再需要CAS來加鎖和解鎖,只需簡(jiǎn)單測(cè)試一下對(duì)象頭的mark?word中偏向鎖線程ID是否是當(dāng)前線程ID,
如果成功,表示線程已獲取到鎖直接進(jìn)入代碼塊運(yùn)行。
如果測(cè)試失敗,檢查當(dāng)前偏向鎖字段是否為0,如果為0,將偏向鎖字段設(shè)置為1,并且更新自己的線程ID到mark?word字段當(dāng)中
如果為1,表示此時(shí)偏向鎖已經(jīng)被別的線程獲取。則此線程不斷嘗試使用CAS獲取偏向鎖或者將偏向鎖撤銷,升級(jí)為輕量級(jí)鎖(升級(jí)概率較大)。
偏向鎖的撤銷:
偏向鎖使用一種等待競(jìng)爭(zhēng)出現(xiàn)才釋放鎖的機(jī)制,當(dāng)有其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放偏向鎖。
小tips:偏向鎖的撤銷開銷較大,需要等待線程進(jìn)入全局安全點(diǎn)safepoint(當(dāng)前線程在CPU上沒有執(zhí)行任何有用字節(jié)碼)
偏向鎖從JDK1.6默認(rèn)開啟,但是它在應(yīng)用程序幾秒后才激活。
-XX:BiasedLockingStartupDelay=0,將延遲關(guān)閉,JVM一啟動(dòng)就激活偏向鎖
-XX:-UseBiasedLocking=false,關(guān)閉偏向鎖,程序默認(rèn)進(jìn)入輕量級(jí)鎖。
偏向鎖的獲得和撤銷流程
2.3?輕量級(jí)鎖? 減少多線程進(jìn)入互斥(mutex)的幾率
輕量級(jí)鎖:多個(gè)線程在不同時(shí)間段請(qǐng)求同一把鎖,也就是基本不存在鎖競(jìng)爭(zhēng)。針對(duì)此種情況,JVM采用輕量級(jí)鎖來避免線程的阻塞以及喚醒。
只要在同一時(shí)間內(nèi)有線程去競(jìng)爭(zhēng)鎖,那么線程執(zhí)行一次CAS操作,然后發(fā)現(xiàn)已經(jīng)被別的線程搶占,直接升級(jí)為重量級(jí)鎖,不在進(jìn)行CAS操作;
輕量級(jí)鎖及膨脹流程圖
加鎖:
線程在執(zhí)行同步代碼塊之前,JVM先在當(dāng)前線程的棧幀中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并將對(duì)象頭的mark?word字段直接復(fù)制到此空間中。然后線程嘗試使用CAS將對(duì)象頭的mark?word替換為指向鎖記錄的指針(指當(dāng)前線程),如果成功表示獲取到輕量級(jí)鎖。如果失敗,表示其他線程競(jìng)爭(zhēng)輕量級(jí)鎖,當(dāng)前線程便使用自旋來不斷嘗試。
釋放:
解鎖時(shí),會(huì)使用CAS將復(fù)制的mark?word替換回對(duì)象頭,如果成功,表示沒有競(jìng)爭(zhēng)發(fā)生,正常解鎖。如果失敗,表示當(dāng)前鎖存在競(jìng)爭(zhēng),進(jìn)一步膨脹為重量級(jí)鎖。
三種鎖的對(duì)比:
| 鎖 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
| 偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法比僅存在納秒級(jí)的差距。 | 如果線程間存在鎖競(jìng)爭(zhēng),會(huì)帶來額外的鎖撤銷的消耗。 | 適用于一個(gè)線程訪問同步代碼塊 |
| 輕量級(jí)鎖 | 競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了程序的響應(yīng)速度。 | 如果始終得不到鎖競(jìng)爭(zhēng)的線程使用自旋會(huì)消耗CPU。 | 響應(yīng)時(shí)間很短,同步塊執(zhí)行速度非???#xff0c;適用于多個(gè)線程在不同時(shí)間段申請(qǐng)同一把鎖 |
| 重量級(jí)鎖 | 線程競(jìng)爭(zhēng)不使用自旋,不會(huì)消耗CPU。 | 線程阻塞,響應(yīng)時(shí)間緩慢。 | 追求吞吐量。 同步塊執(zhí)行速度較長(zhǎng)。 |
?
重量級(jí)鎖會(huì)阻塞,喚醒請(qǐng)求加鎖的線程。針對(duì)的是多個(gè)線程同一個(gè)時(shí)刻競(jìng)爭(zhēng)同一把鎖的情況,JVM采用自適應(yīng)自旋,來避免線程在面對(duì)非常小的同步塊時(shí),仍會(huì)被阻塞以及喚醒。
輕量級(jí)鎖采用CAS操作,將鎖對(duì)象的標(biāo)記字段替換為指向線程的指針,存儲(chǔ)著鎖對(duì)象原本的標(biāo)記字段。針對(duì)的是多個(gè)線程在不同時(shí)間段申請(qǐng)同一把鎖的情況。
偏向鎖只會(huì)在第一次請(qǐng)求時(shí)采用CAS操作,在鎖對(duì)象的mark?word字段中記錄下當(dāng)前線程ID,此后運(yùn)行中持有偏向鎖的線程不再有加鎖過程。針對(duì)的是鎖僅會(huì)被同一線程持有
總結(jié)
以上是生活随笔為你收集整理的java中的CAS操作以及锁机制详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: FastAPI 对MySQL 数据库的操
- 下一篇: 幼儿抽象逻辑思维举例_为什么说幼儿教育非