Java多線程之悲觀鎖與樂觀鎖
問題:
1、樂觀鎖和悲觀鎖的理解及如何實(shí)現(xiàn),有哪些實(shí)現(xiàn)方式?
2、什么是樂觀鎖和悲觀鎖?
3、樂觀鎖可以重入嗎?
1. 悲觀鎖存在的問題
獨(dú)占鎖其實(shí)就是一種悲觀鎖,java的synchronized是悲觀鎖。悲觀鎖可以確保無(wú)論哪個(gè)線程持有鎖,都能獨(dú)占式訪問臨界區(qū)。雖然悲觀鎖的邏輯非常簡(jiǎn)單,但是存在不少問題。
悲觀鎖總是假設(shè)會(huì)發(fā)生最壞的情況,每次線程讀取數(shù)據(jù)時(shí),也會(huì)上鎖。這樣其他線程在讀取數(shù)據(jù)時(shí)就會(huì)被阻塞,直到它拿到鎖。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)用到了很多悲觀鎖,比如行鎖、表鎖、讀鎖、寫鎖等。
悲觀鎖機(jī)制存在以下問題:
(1)在多線程競(jìng)爭(zhēng)下,加鎖、釋放鎖會(huì)導(dǎo)致比較多的上下文切換和調(diào)度延時(shí),引起性能問題。
(2)一個(gè)線程持有鎖后,會(huì)導(dǎo)致其他所有搶占此鎖的線程掛起。
(3)如果一個(gè)優(yōu)先級(jí)高的線程等待一個(gè)優(yōu)先級(jí)低的線程釋放鎖,就會(huì)導(dǎo)致線程的優(yōu)先級(jí)倒置,從而引發(fā)性能風(fēng)險(xiǎn)。
解決以上悲觀鎖的這些問題的有效方式是使用樂觀鎖去替代悲觀鎖。與之類似,數(shù)據(jù)庫(kù)操作中的帶版本號(hào)數(shù)據(jù)更新、JUC包的原子類,都使用了樂觀鎖的方式提升性能。
2. 通過CAS實(shí)現(xiàn)樂觀鎖
樂觀鎖的操作主要就是兩個(gè)步驟:(1)第一步:沖突檢測(cè)。(2)第二步:數(shù)據(jù)更新。
樂觀鎖一種比較典型的就是CAS原子操作,JUC強(qiáng)大的高并發(fā)性能是建立在CAS原子之上的。CAS操作中包含三個(gè)操作數(shù):需要操作的內(nèi)存位置(V)、進(jìn)行比較的預(yù)期原值(A)和擬寫入的新值(B)。如果內(nèi)存位置V的值與預(yù)期原值A(chǔ)相匹配,那么處理器會(huì)自動(dòng)將該位置的值更新為新值B;否則處理器不做任何操作。
CAS操作可以非常清晰地分為兩個(gè)步驟:
(1)檢測(cè)位置V的值是否為A。
(2)如果是,就將位置V更新為B值;否則不要更改該位置。
CAS操作的兩個(gè)步驟其實(shí)與樂觀鎖操作的兩個(gè)步驟是一致的,都是在沖突檢測(cè)后進(jìn)行數(shù)據(jù)更新。
樂觀鎖是一種思想,而CAS是這種思想的一種實(shí)現(xiàn)。實(shí)際上,如果需要完成數(shù)據(jù)的最終更新,僅僅進(jìn)行一次CAS操作是不夠的,一般情況下,需要進(jìn)行自旋操作,即不斷地循環(huán)重試CAS操作直到成功,這也叫CAS自旋。通過CAS自旋,在不使用鎖的情況下實(shí)現(xiàn)多線程之間的變量同步,也就是說,在沒有線程被阻塞的情況下實(shí)現(xiàn)變量的同步,這叫作“非阻塞同步”,或者說“無(wú)鎖同步”。使用基于CAS自旋的樂觀鎖進(jìn)行同步控制,屬于無(wú)鎖編程的一種實(shí)踐。
3. 不可重入的自旋鎖
自旋鎖的基本含義為:當(dāng)一個(gè)線程在獲取鎖的時(shí)候,如果鎖已經(jīng)被其他線程獲取,調(diào)用者就一直在那里循環(huán)檢查該鎖是否已經(jīng)被釋放,一直到獲取到鎖才會(huì)退出循環(huán)。
CAS自旋鎖的實(shí)現(xiàn)原理為:
搶鎖線程不斷進(jìn)行CAS自旋操作去更新鎖的owner(擁有者),如果更新成功,就表明已經(jīng)搶鎖成功,退出搶鎖方法。如果鎖已經(jīng)被其他線程獲?。ㄒ簿褪莖wner為其他線程),調(diào)用者就一直在那里循環(huán)進(jìn)行owner的CAS更新操作,一直到成功才會(huì)退出循環(huán)。
public class SpinLock implements Lock { // 當(dāng)前鎖的擁有者 private AtomicReference<Thread> owner = new AtomicReference<>(); @Override public void lock() { Thread t = Thread.currentThread(); // 自旋 while (owner.compareAndSet(null,t)){ // 讓出CPU的時(shí)間片 Thread.yield(); } } @Override public void unlock() { Thread t = Thread.currentThread(); // 只有擁有者才能獲取鎖 if(t==owner.get()){ // 設(shè)置owner為空,這里不需要使用compareAndSet,因?yàn)橐呀?jīng)通過owner做過線程檢查 owner.set(null); } } // 省略其他代碼... }
上述SpinLock是不支持重入的,即當(dāng)一個(gè)線程第一次已經(jīng)獲取到了該鎖,在鎖沒有被釋放之前,如果又一次重新獲取該鎖,第二次將不能成功獲取到,因?yàn)樽孕驝AS會(huì)失敗。
4. 可重入的自旋鎖
為了實(shí)現(xiàn)可重入鎖,這里引入一個(gè)計(jì)數(shù)器,用來記錄一個(gè)線程獲取鎖的次數(shù)。一個(gè)簡(jiǎn)單的可重入的自旋鎖的代碼大致如下:
public class ReentrantSpinLock implements Lock { // 當(dāng)前鎖的擁有者,使用Thread作為同步狀態(tài) AtomicReference<Thread> owner = new AtomicReference<>(); // 記錄一個(gè)線程重復(fù)獲取鎖的次數(shù) private int count = 0; // 搶占鎖 @Override public void lock() { Thread t = Thread.currentThread(); // 如果時(shí)沖入,增加重入次數(shù)后,返回 if(t==owner.get()){ count++; return; } // 自旋 while (owner.compareAndSet(null,t)){ Thread.yield(); } } @Override public void unlock() { Thread t = Thread.currentThread(); // 只有擁有者才能釋放鎖 if(t==owner.get()){ // 如果重入次數(shù)大于0,減少重入次數(shù)后返回 if(count>0){ count--; }else{ // 設(shè)置擁有者為null owner.set(null); } } } // 省略其他代碼... }
自旋鎖的特點(diǎn):線程獲取鎖的時(shí)候,如果鎖被其他線程持有,當(dāng)前線程將循環(huán)等待,直到獲取到鎖。線程搶鎖期間狀態(tài)不會(huì)改變,一直是運(yùn)行狀態(tài)(RUNNABLE),在操作系統(tǒng)層面線程處于用戶態(tài)。
自旋鎖的問題:在爭(zhēng)用激烈的場(chǎng)景下,如果某個(gè)線程持有鎖的時(shí)間太長(zhǎng),就會(huì)導(dǎo)致其他空自旋的線程耗盡CPU資源。另外,如果大量的線程進(jìn)行空自旋,還可能導(dǎo)致硬件層面的“總線風(fēng)暴”。
在爭(zhēng)用激烈的場(chǎng)景下,Java輕量級(jí)鎖會(huì)快速膨脹為重量級(jí)鎖,其本質(zhì)上一是為了減少CAS空自旋,二是為了避免同一時(shí)間大量CAS操作所導(dǎo)致的總線風(fēng)暴。那么,JUC基于CAS實(shí)現(xiàn)的輕量級(jí)鎖如何避免總線風(fēng)暴呢?答案是:使用隊(duì)列對(duì)搶鎖線性進(jìn)行排隊(duì),最大程度上減少了CAS操作數(shù)量。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
一文搞懂SpringBoot如何利用@Async實(shí)現(xiàn)異步調(diào)用
異步調(diào)用幾乎是處理高并發(fā),解決性能問題常用的手段,如何開啟異步調(diào)用?SpringBoot中提供了非常簡(jiǎn)單的方式,就是一個(gè)注解@Async。今天我們重新認(rèn)識(shí)一下@Async,以及注意事項(xiàng)2022-09-09使用Java橋接模式打破繼承束縛優(yōu)雅實(shí)現(xiàn)多維度變化
這篇文章主要為大家介紹了使用Java橋接模式打破繼承束縛,優(yōu)雅實(shí)現(xiàn)多維度變化,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05Java字節(jié)與字符流永久存儲(chǔ)json數(shù)據(jù)
本篇文章給大家詳細(xì)講述了Java字節(jié)與字符流永久存儲(chǔ)json數(shù)據(jù)的方法,以及代碼分享,有興趣的參考學(xué)習(xí)下。2018-02-02Spring Boot定時(shí)任務(wù)單線程多線程實(shí)現(xiàn)代碼解析
這篇文章主要介紹了Spring Boot定時(shí)任務(wù)單線程多線程實(shí)現(xiàn)代碼解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08uploadify java實(shí)現(xiàn)多文件上傳和預(yù)覽
這篇文章主要為大家詳細(xì)介紹了java結(jié)合uploadify實(shí)現(xiàn)多文件上傳和預(yù)覽的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10Java源碼解析阻塞隊(duì)列ArrayBlockingQueue介紹
今天小編就為大家分享一篇關(guān)于Java源碼解析阻塞隊(duì)列ArrayBlockingQueue介紹,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-01-01通過jenkins發(fā)布java項(xiàng)目到目標(biāo)主機(jī)上的詳細(xì)步驟
這篇文章主要介紹了通過jenkins發(fā)布java項(xiàng)目到目標(biāo)主機(jī)上的詳細(xì)步驟,發(fā)布java項(xiàng)目的步驟很簡(jiǎn)單,通過拉取代碼并打包,備份目標(biāo)服務(wù)器上已有的要發(fā)布項(xiàng)目,具體內(nèi)容詳情跟隨小編一起看看吧2021-10-10Java從JDK源碼角度對(duì)Object進(jìn)行實(shí)例分析
這篇文章主要介紹了Java從JDK源碼角度對(duì)Object進(jìn)行實(shí)例分析,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12