
您好,我是南橘,萬法仙門的掌門,剛剛從九州世界穿越到地球,因為時空亂流的影響導(dǎo)致我的法力全失,現(xiàn)在不得不通過這個平臺向廣大修真天才們借去力量。你們的每一個點贊,每一個關(guān)注都是讓我回到九州世界的助力,兄弟萌來為我注入修為吧!關(guān)注WX號:南橘ryc
今天是平安夜,祝大家都有一個愉快的夜晚。
“羅妍師姐!研究院中研究元宇宙的元嬰真人羅銘志剛剛渡劫失敗,差點隕落了?!弊鳛閮墒勒校钚「旧喜粫鲈葡龅?,但是總能及時的獲取門內(nèi)各種八卦消息。
“哦,我知道,他是我哥。”二師姐羅妍永遠是一副冷冰冰的面孔,但是整個萬法仙門都知道她其實經(jīng)常半夜在后山唱歌。
“額?!崩钚「械接幸稽c尷尬,連忙把手中剛剛從冰庫取出來的西瓜分了一半給羅妍:“那您大哥他沒事吧?!?/p>
“死不了。”羅妍接過半囊西瓜,捏了一個法決變出一根勺子開始挖西瓜吃:“最后一道劫雷下來之前,他已經(jīng)用鎖連人帶天劫給鎖住了。后來掌門出手解決了這件事,不過嘛,在床上躺上三五個月是很正常的。”
“什么鎖這么神奇?還能鎖住天劫?”
“小庚同學(xué),我們?nèi)f法仙門《Java真經(jīng)》中的鎖,可是包羅萬象的哦?!痹菩∠霾恢裁磿r候突然出現(xiàn),一把奪過李小庚手里剩下的半個瓜,囂張的吃了一大口:“小羅妍,給小庚講講咱鎖吧?!?/p>
“好的師父。”
一、樂觀鎖 VS 悲觀鎖
在Java中,我們能接觸到各種各樣的鎖,而每種鎖因其特性的不同,在不同的的場景下有著不同的效果。
悲觀鎖和樂觀鎖大概是我們聽到最多的兩種鎖了,這兩種鎖的區(qū)分更多的是思想上。
對于一個操作,悲觀鎖認為自己在操作過程中,一定有別的線程也要來修改這個數(shù)據(jù),所以一定會加鎖。而樂觀鎖則不認為會有別的線程來干擾自己,所以不需要加鎖。
在Java中,synchronized關(guān)鍵字和Lock的實現(xiàn)類都是悲觀鎖,而樂觀鎖一般采用無鎖編程,也就是CAS算法來實現(xiàn)的。
1、1、悲觀鎖
image
悲觀鎖的實現(xiàn):
- 1、線程嘗試去獲取鎖
- 2、線程加鎖成功并執(zhí)行操作,其他線程等待,線程加鎖失敗則等待獲取鎖(這里有好幾種辦法,在synchronized中,會有在四種狀態(tài)中改變,在下文中我會介紹這四種情況)
- 3、線程執(zhí)行完畢釋放鎖,其他線程獲取鎖
通過圖片和文字,我們能看出悲觀鎖適合寫操作多的場景,加鎖可以確保數(shù)據(jù)的安全,但是會影響一些操作效率。
1、2、樂觀鎖
image
這兩張圖是從這位大佬的文章中引用的:不可不說的Java“鎖”事 - 美團技術(shù)團隊樂觀鎖的實現(xiàn):
- 1、線程直接獲取同步資源數(shù)據(jù)
- 2、判斷內(nèi)存中的同步數(shù)據(jù)是否被其他線程修改
- 3、沒有被修改則直接更新
- 4、如果被其他線程修則選擇報錯或者重試(自旋)
和悲觀鎖不同,樂觀鎖明顯不適合經(jīng)常進行修改,因為誰也不能保證不會出現(xiàn)數(shù)據(jù)安全的問題,所以樂觀鎖適合讀操作的場景。對于讀操作來說,加鎖只會影響效率。
上文說到了,樂觀鎖一般采用CAS算法來實現(xiàn),那么我們就來講講什么是CAS算法
1、3、CAS算法
CAS的英語是【Compare and Swap】,比較和交換,單單從這一個詞組來看,我們就已經(jīng)能Get到CAS算法的核心了。
CAS的算法涉及三個操作數(shù): 內(nèi)存位置(V)、預(yù)期原值(A)、新值(B)。
如果內(nèi)存位置的值與預(yù)期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。
換一種說法,當且僅當 V 的值等于 A時,CAS通過原子方式用新值B來更新V的值(“比較+更新”整體是一個原子操作),否則不會執(zhí)行任何操作。
在JDK1.5 中新增java.util.concurrent(J.U.C)就是通過實現(xiàn)CAS來實現(xiàn)樂觀鎖的。
我們可以看一下它的重點:image
在沒有鎖的機制下需要字段value要借助volatile原語,保證線程間的數(shù)據(jù)是可見的。這樣在獲取變量的值的時候才能直接讀取,這就是內(nèi)存的可見性。
image
imageimage
從上面這三個圖可以看出,CAS每次從內(nèi)存中讀取數(shù)據(jù)然后將此數(shù)據(jù)修改+1后的結(jié)果進行CAS操作比較,如果成功就返回結(jié)果,否則重試直到成功為止,compareAndSet利用JNI來完成CPU指令的操作。
是不是很復(fù)雜?其實一點也不復(fù)雜,我們可以這樣理解:CPU去更新一個值,但如果想改的值和原來的值,操作就失?。ㄒ驗橛衅渌僮飨雀淖兞诉@個值),然后可以去再次嘗試。如果想改的值和原來一樣,那么就修改之。
但是,CAS也有一些問題
- ABA問題
一個線程X1從內(nèi)存位置V中取出A,這時候另一個線程Y1也從內(nèi)存中取出A,并且Y1進行了一些操作變成了B,然后Y1又將V位置的數(shù)據(jù)變成A,這時候線程X1進行CAS操作發(fā)現(xiàn)內(nèi)存中仍然是A,然后X1操作成功。盡管線程X1的CAS操作成功,但是不代表這個過程就是沒有問題的。
解決辦法:
JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,具體操作封裝在compareAndSet()中,利用JNI來檢查當前引用是否等于預(yù)期引用,并且當前標志是否等于預(yù)期標志,如果全部相等,則以原子方式將該引用和該標志的值設(shè)置為給定的更新值。image
循環(huán)時間長開銷大
CAS操作如果長時間不成功,會導(dǎo)致其一直自旋,給CPU帶來非常大的開銷。只能保證一個共享變量的原子操作
Java從1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,可以把多個變量放在一個對象里來進行CAS操作。Java中的線程安全問題至關(guān)重要,要想保證線程安全,就需要用到樂觀鎖與悲觀鎖。悲觀鎖是獨占鎖,阻塞鎖。樂觀鎖是非獨占鎖,非阻塞鎖。什么情況選擇什么樣的鎖,就是我們開發(fā)人員需要思考的問題了。
二、自旋鎖VS非自旋鎖
我們之前提到了CAS操作如果長時間不成功,會導(dǎo)致其一直自旋,非常浪費性能。但是實際是,自旋是非常有用的。
自旋鎖(spinlock):是指當一個線程在獲取鎖的時候,如果鎖已經(jīng)被其它線程獲取,那么該線程將循環(huán)等待,然后不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出循環(huán)。
自旋鎖不會放棄CUP時間片,而是通過自旋等待鎖釋放。
為什么要自旋,?獲取鎖的線程一直處于活躍狀態(tài),但是并沒有執(zhí)行任何有效的任務(wù),使用這種鎖不是會造成busy-waiting嗎?
因為在我們的程序中,如果存在著大量的互斥同步代碼,當出現(xiàn)高并發(fā)的時候,系統(tǒng)內(nèi)核態(tài)就需要不斷的去掛起線程和恢復(fù)線程,頻繁的上下文切換會對我們系統(tǒng)的并發(fā)性能有一定影響。在程序的執(zhí)行過程中鎖定“共享資源“的時間片是極短的,如果僅僅是為了這點時間而去不斷掛起、恢復(fù)線程的話,消耗的時間可能會更長,那就“撿了芝麻丟了西瓜”了。
image自旋等待雖然避免了線程切換的開銷,但它要占用處理器時間。如果鎖被占用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被占用的時間很長,那么自旋的線程只會白浪費處理器資源
于是乎,自適應(yīng)的自旋鎖出現(xiàn)了。
自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啟。JDK 6中變?yōu)槟J開啟,并且引入了自適應(yīng)的自旋鎖(適應(yīng)性自旋鎖)。
自適應(yīng)的自旋鎖
自適應(yīng)自旋鎖的出現(xiàn)使得自旋操作變得聰明起來,不再跟之前一樣死板。所謂的“自適應(yīng)”意味著對于同一個鎖對象,線程的自旋時間是根據(jù)上一個持有該鎖的線程的自旋時間以及狀態(tài)來確定的。例如對于A鎖對象來說,如果一個線程剛剛通過自旋獲得到了鎖,并且該線程也在運行中,那么JVM會認為此次自旋操作也是有很大的機會可以拿到鎖,因此它會讓自旋的時間相對延長。但是如果對于B鎖對象自旋操作很少成功的話,JVM甚至可能直接忽略自旋操作。
因此,自適應(yīng)自旋鎖在一定程度上能強化自旋鎖的性能。
可是,出現(xiàn)了多個線程同時爭搶鎖資源,我們也不能總是自旋??!
于是,java團隊又進行了進化。
三、無鎖 VS 偏向鎖 VS 輕量級鎖 VS 重量級鎖
學(xué)習這四個鎖之前,我們先來了解一下java對象頭和Monitor的概念。
synchronized是悲觀鎖,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在Java對象頭里的,Hotspot的對象頭主要包括兩部分數(shù)據(jù):Mark Word(標記字段)、Klass Pointer(類型指針)。
- Mark Word:默認存儲對象的HashCode,分代年齡和鎖標志位信息。
- Klass Point:對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
每一個Java對象就有一把看不見的鎖,稱為內(nèi)部鎖或者Monitor鎖。
Monitor是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表,每一個被鎖住的對象都會和一個monitor關(guān)聯(lián),同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程占用。
synchronized通過Monitor來實現(xiàn)線程同步,Monitor是依賴于底層的操作系統(tǒng)的Mutex Lock(互斥鎖)來實現(xiàn)的線程同步
為了了解這幾個概念,我們可以通過兩個代碼來看一個:
image
image
第一塊代碼很簡單,看一看字節(jié)碼,非常清楚,一眼就能看出它做了什么。
image
image
再看第二個代碼,看看java代碼,非常簡單,和HelloWorld相比只是多了一個synchronize的代碼塊,但是字節(jié)碼卻大不一樣,可以看出在加鎖的代碼塊, 多了個 monitorenter , monitorexit。每個對象有一個監(jiān)視器鎖(monitor)。當monitor被占用時就會處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時嘗試獲取monitor的所有權(quán),過程如下:
- 1、如果monitor的進入數(shù)為0,則該線程進入monitor,然后將進入數(shù)設(shè)置為1,該線程即為monitor的所有者。
- 2、如果線程已經(jīng)占有該monitor,只是重新進入,則進入monitor的進入數(shù)加1.
- 3、如果其他線程已經(jīng)占用了monitor,則該線程進入阻塞狀態(tài),直到monitor的進入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)
執(zhí)行monitorexit的線程必須是objectref所對應(yīng)的monitor的所有者:
- 1、指令執(zhí)行時,monitor的進入數(shù)減1
- 2、如果減1后進入數(shù)為0,那線程退出monitor,不再是這個monitor的所有者
- 3、其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權(quán)
通過這兩個圖,大家大概就能理解之前的那兩個概念了。
我們知道,高并發(fā)的情況,不斷地爭搶鎖,系統(tǒng)內(nèi)核態(tài)就需要不斷的去掛起線程和恢復(fù)線程,頻繁的上下文切換會對我們系統(tǒng)的并發(fā)性能有一定影響。如果同步代碼塊中的內(nèi)容過于簡單,狀態(tài)轉(zhuǎn)換消耗的時間有可能比用戶代碼執(zhí)行的時間還要長”。這種方式就是synchronized最初實現(xiàn)同步的方式,這就是JDK 6之前synchronized效率低的原因。這種依賴于操作系統(tǒng)Mutex Lock所實現(xiàn)的鎖我們稱之為“重量級鎖”,JDK6中為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”。
所以目前鎖一共有4種狀態(tài),級別從低到高依次是:無鎖、偏向鎖、輕量級鎖和重量級鎖。鎖狀態(tài)只能升級不能降級。
這是四種鎖狀態(tài)對應(yīng)的的:Mark Word(標記字段)內(nèi)容:
| 鎖狀態(tài) | 存儲內(nèi)容 | Mark Word |
|---|---|---|
| 無鎖 | 對象的hashCode、對象分代年齡、是否是偏向鎖(0) | 01 |
| 偏向鎖 | 偏向線程ID、偏向時間戳、對象分代年齡、是否是偏向鎖(1) | 01 |
| 輕量級鎖 | 指向棧中鎖記錄的指針 | 00 |
| 重量級鎖 | 指向互斥量(重量級鎖)的指針 | 10 |
3、1無鎖
無鎖的特點就是修改操作在循環(huán)內(nèi)進行,線程會不斷的嘗試修改共享資源。
如果有多個線程修改同一個值,必定會有一個線程能修改成功,而其他修改失敗的線程會不斷重試直到修改成功,CAS原理及應(yīng)用即是無鎖的實現(xiàn)。
3、2偏向鎖
偏向鎖是指一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖,降低獲取鎖的代價。
偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態(tài)。撤銷偏向鎖后恢復(fù)到無鎖(標志位為“01”)或輕量級鎖(標志位為“00”)的狀態(tài)。
3、3輕量級鎖
當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。
在代碼進入同步塊的時候,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,然后拷貝對象頭中的Mark Word復(fù)制到鎖記錄中。
拷貝成功后,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock Record里的owner指針指向?qū)ο蟮腗ark Word。
如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位設(shè)置為“00”,表示此對象處于輕量級鎖定狀態(tài)。
如果輕量級鎖的更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進入同步塊繼續(xù)執(zhí)行,否則說明多個線程競爭鎖。
若當前只有一個等待線程,則該線程通過自旋進行等待。但是當自旋超過一定的次數(shù),或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級為重量級鎖。
3、4重量級鎖
升級為重量級鎖時,鎖標志的狀態(tài)值變?yōu)椤?0”,此時Mark Word中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進入阻塞狀態(tài)。
四、公平鎖 VS 非公平鎖
4、1公平鎖
公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖。
公平鎖的優(yōu)點是:等待鎖的線程不會餓死,人人有飯吃,人人有書讀
公平鎖的缺點是:整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大
4、2、非公平鎖
非公平鎖是多個線程加鎖時直接嘗試獲取鎖,獲取不到才會到等待隊列的隊尾等待。如果此時此刻鎖剛好可用,那么這個線程就可以插隊,無阻塞地獲取鎖。
非公平鎖的優(yōu)點是:可以減少喚起線程的開銷,整體的吞吐效率高,因為線程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程
非公平鎖的缺點是:處于等待隊列中的線程可能會餓死,或者等很久才會獲得鎖
我們可以通過一些源碼來看一看公平鎖和非公平鎖在java中的應(yīng)用。
公平鎖FairSync和非公平鎖NonfairSync的代碼
image
從結(jié)構(gòu)中來看,ReentrantLock里面有一個內(nèi)部類Sync,Sync繼承自AQS(AbstractQueuedSynchronizer),添加鎖和釋放鎖的大部分操作實際上都是在Sync中實現(xiàn)的。它有公平鎖FairSync和非公平鎖NonfairSync兩個子類。ReentrantLock默認使用非公平鎖,也可以通過構(gòu)造器來顯示的指定使用公平鎖。
公平鎖FairSync 非公平鎖NonfairSync image image我們用軟件比較一下:
image是不是很清晰了?公平鎖和非公平鎖只有一個地方不一樣
image閱讀一下注釋:是否返回true取決于頭是否在尾部之前初始化以及頭是否準確(如果當前線程在隊列中)
意思就是這個方法主要是判斷當前線程是否位于同步隊列中的第一個。如果是則返回true,否則返回false
由此可得,公平鎖通過同步隊列來實現(xiàn)順序獲取鎖,而非公平鎖加鎖時不考慮先后順序,直接嘗試去獲取鎖,所以存在后申請卻先獲得鎖的情況。
五、可重入鎖 VS 非可重入鎖
可重入鎖這個概念也比較好理解,在同一個線程在外層方法獲取鎖的時候,再進入該線程的內(nèi)層方法能自動獲取鎖(前提鎖對象得是同一個對象或者class)就是可重入鎖,不能自動獲取那么這個鎖就是不可重入鎖。
在JAVA中,我們最熟悉的ReentrantLock和synchronized都是可重入鎖。
為什么可重入鎖可以自動獲得鎖呢?
可重入鎖ReentrantLock:
image
不可重入鎖NonReentrantLock:
image這兩個圖是不是很明顯?
由圖可知,當線程嘗試獲取鎖時,可重入鎖先嘗試獲取并更新status值,如果status == 0表示沒有其他線程在執(zhí)行同步代碼,則把status置為1,當前線程開始執(zhí)行。如果status != 0,則判斷當前線程是否是獲取到這個鎖的線程,如果是的話執(zhí)行status+1,且當前線程可以再次獲取鎖。而非可重入鎖是直接去獲取并嘗試更新當前status的值,如果status != 0的話會導(dǎo)致其獲取鎖失敗,當前線程阻塞。
釋放鎖時,可重入鎖同樣先獲取當前status的值,在當前線程是持有鎖的線程的前提下。如果status-1 == 0,則表示當前線程所有重復(fù)獲取鎖的操作都已經(jīng)執(zhí)行完畢,然后該線程才會真正釋放鎖。而非可重入鎖則是在確定當前線程是持有鎖的線程之后,直接將status置為0,將鎖釋放。
六、獨享鎖 VS 共享鎖
獨享鎖和共享鎖這個概念,可以類比為讀寫鎖。
舉個例子,A線程獲得數(shù)據(jù)ZZZ的鎖,如果加鎖后其他的線程不能再對ZZZ加任何形式的鎖,也不能對它進行讀寫,那么說明ZZZ上的是排他鎖。
如果線程A獲得數(shù)據(jù)ZZZ上的鎖以后,則其他線程還能對ZZZ再加共享鎖,獲得共享鎖的線程還能讀數(shù)據(jù),只是不能修改數(shù)據(jù),那么說明ZZZ上的是共享鎖。
我們可以看看讀寫鎖ReentrantReadWriteLock
image讀寫鎖里面有兩把鎖,一把是ReadLock,一把是WriteLock,現(xiàn)在我們不知道里面是什么樣子的
ReadLock:
image
WriteLock:
image我們驚訝的發(fā)現(xiàn)了一個老熟人state,我們總是能看到他。
在獨享鎖中state這個值通常是0或者1(如果是重入鎖的話state值就是重入的次數(shù)),在共享鎖中state就是持有鎖的數(shù)量。但是在ReentrantReadWriteLock中有讀、寫兩把鎖,所以需要在一個整型變量state上分別描述讀鎖和寫鎖的數(shù)量(或者也可以叫狀態(tài))。于是將state變量“按位切割”切分成了兩個部分,高16位表示讀鎖狀態(tài)(讀鎖個數(shù)),低16位表示寫鎖狀態(tài)(寫鎖個數(shù))。
imageimage
從寫鎖的這一段我們可以看出,它首先判斷是否已經(jīng)有線程持有了鎖。如果已經(jīng)有線程持有了鎖(c!=0),則查看當前寫鎖線程的數(shù)目,如果寫線程數(shù)為0(即此時存在讀鎖)或者持有鎖的線程不是當前線程就返回失敗。
image
從讀鎖中又能發(fā)現(xiàn),如果其他線程已經(jīng)獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態(tài)。如果當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增加讀狀態(tài),成功獲取讀鎖。
“鎖的功能和用法竟然有這么多的嗎?”李小庚長吸了一口冷氣,本來只是簡單地吃一個瓜,沒想到被知識大禮包砸中了。
“其實我們的《Java真經(jīng)》本身已經(jīng)對鎖本身進行了良好的封裝,降低了斗法中使用難度,這也是我那個倒霉哥哥能活下來的原因?!绷_妍舀完了最后一塊瓜肉,滿足的伸了個懶腰:“好了師弟,即使封裝的再好,熟悉鎖的底層原理,才能在不同場景下選擇最適合的鎖。平常在修行的過程中,不要只追求結(jié)果的實現(xiàn),多研究研功法究源碼才能讓你對它的理解更加深刻。”說罷,一個轉(zhuǎn)身便向?qū)嶒炇易呷ァ?/p>
“你二師姐可是咱萬法仙門出了名的愛專研,經(jīng)常能夠發(fā)現(xiàn)功法中的漏洞?!痹菩∠龊翢o風度的蹲在一邊,感嘆道:“所以人家才能在高手云集的結(jié)丹組大比中獲得的冠軍??!”
“嘿嘿,下屆的冠軍就是我了。”
“哦,是嗎?被一招秒殺的筑基組亞軍李小庚同學(xué)。”
“喂喂喂,別揭短行不行?!?/p>