網上關於Java中鎖的話題可以說資料相當豐富,但相關內容總感覺是一大串術語的羅列,讓人雲里霧里,讀完就忘。本文希望能為Java新人做一篇通俗易懂的整合,旨在消除對各種各樣鎖的術語的恐懼感,對每種鎖的底層實現淺嘗輒止,但是在需要時能夠知道去查什么。
首先要打消一種想法,就是一個鎖只能屬於一種分類。其實並不是這樣,比如一個鎖可以同時是悲觀鎖、可重入鎖、公平鎖、可中斷鎖等等,就像一個人可以是男人、醫生、健身愛好者、游戲玩家,這並不矛盾。OK,國際慣例,上干貨。
〇、synchronized與Lock
Java中有兩種加鎖的方式:一種是用synchronized關鍵字,另一種是用Lock接口的實現類。
形象地說,synchronized關鍵字是自動檔,可以滿足一切日常駕駛需求。但是如果你想要玩漂移或者各種騷操作,就需要手動檔了——各種Lock的實現類。
所以如果你只是想要簡單的加個鎖,對性能也沒什么特別的要求,用synchronized關鍵字就足夠了。自Java 5之后,才在java.util.concurrent.locks包下有了另外一種方式來實現鎖,那就是Lock。也就是說,synchronized是Java語言內置的關鍵字,而Lock是一個接口,這個接口的實現類在代碼層面實現了鎖的功能,具體細節不在本文展開,有興趣可以研究下AbstractQueuedSynchronizer類,寫得可以說是牛逼爆了。
其實只需要關注三個類就可以了:ReentrantLock類、ReadLock類、WriteLock類。
ReentrantLock、ReadLock、WriteLock 是Lock接口最重要的三個實現類。對應了“可重入鎖”、“讀鎖”和“寫鎖”,后面會講它們的用途。
ReadWriteLock其實是一個工廠接口,而ReentrantReadWriteLock是ReadWriteLock的實現類,它包含兩個靜態內部類ReadLock和WriteLock。這兩個靜態內部類又分別實現了Lock接口。
我們停止深究源碼,僅從使用的角度看,Lock與synchronized的區別是什么?在接下來的幾個小節中,我將梳理各種鎖分類的概念,以及synchronized關鍵字、各種Lock實現類之間的區別與聯系。
一、悲觀鎖與樂觀鎖
鎖的一種宏觀分類方式是悲觀鎖和樂觀鎖。悲觀鎖與樂觀鎖並不是特指某個鎖(Java中沒有哪個Lock實現類就叫PessimisticLock或OptimisticLock),而是在並發情況下的兩種不同策略。
悲觀鎖(Pessimistic Lock), 就是很悲觀,每次去拿數據的時候都認為別人會修改。所以每次在拿數據的時候都會上鎖。這樣別人想拿數據就被擋住,直到悲觀鎖被釋放。
樂觀鎖(Optimistic Lock), 就是很樂觀,每次去拿數據的時候都認為別人不會修改。所以不會上鎖,不會上鎖!但是如果想要更新數據,則會在更新前檢查在讀取至更新這段時間別人有沒有修改過這個數據。如果修改過,則重新讀取,再次嘗試更新,循環上述步驟直到更新成功(當然也允許更新失敗的線程放棄操作)。
悲觀鎖阻塞事務,樂觀鎖回滾重試,它們各有優缺點,不要認為一種一定好於另一種。像樂觀鎖適用於寫比較少的情況下,即沖突真的很少發生的時候,這樣可以省去鎖的開銷,加大了系統的整個吞吐量。但如果經常產生沖突,上層應用會不斷的進行重試,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適。
二、樂觀鎖的基礎——CAS
說到樂觀鎖,就必須提到一個概念:CAS
什么是CAS呢?Compare-and-Swap,即比較並替換,也有叫做Compare-and-Set的,比較並設置。
1、比較:讀取到了一個值A,在將其更新為B之前,檢查原值是否仍為A(未被其他線程改動)。
2、設置:如果是,將A更新為B,結束。[1]如果不是,則什么都不做。
上面的兩步操作是原子性的,可以簡單地理解為瞬間完成,在CPU看來就是一步操作。
有了CAS,就可以實現一個樂觀鎖:
data = 123; // 共享數據 /* 更新數據的線程會進行如下操作 */ flag = true; while (flag) { oldValue = data; // 保存原始數據 newValue = doSomething(oldValue); // 下面的部分為CAS操作,嘗試更新data的值 if (data == oldValue) { // 比較 data = newValue; // 設置 flag = false; // 結束 } else { // 啥也不干,循環重試 } } /* 很明顯,這樣的代碼根本不是原子性的, 因為真正的CAS利用了CPU指令, 這里只是為了展示執行流程,本意是一樣的。 */
這是一個簡單直觀的樂觀鎖實現,它允許多個線程同時讀取(因為根本沒有加鎖操作),但是只有一個線程可以成功更新數據,並導致其他要更新數據的線程回滾重試。 CAS利用CPU指令,從硬件層面保證了操作的原子性,以達到類似於鎖的效果。
Java中真正的CAS操作調用的native方法
因為整個過程中並沒有“加鎖”和“解鎖”操作,因此樂觀鎖策略也被稱為無鎖編程。換句話說,樂觀鎖其實不是“鎖”,它僅僅是一個循環重試CAS的算法而已!
三、自旋鎖
有一種鎖叫自旋鎖。所謂自旋,說白了就是一個 while(true) 無限循環。
剛剛的樂觀鎖就有類似的無限循環操作,那么它是自旋鎖嗎?
感謝評論區 養貓的蝦的指正。
不是。盡管自旋與 while(true) 的操作是一樣的,但還是應該將這兩個術語分開。“自旋”這兩個字,特指自旋鎖的自旋。
然而在JDK中並沒有自旋鎖(SpinLock)這個類,那什么才是自旋鎖呢?讀完下個小節就知道了。
四、synchronized鎖升級:偏向鎖 → 輕量級鎖 → 重量級鎖
前面提到,synchronized關鍵字就像是汽車的自動檔,現在詳細講這個過程。一腳油門踩下去,synchronized會從無鎖升級為偏向鎖,再升級為輕量級鎖,最后升級為重量級鎖,就像自動換擋一樣。那么自旋鎖在哪里呢?這里的輕量級鎖就是一種自旋鎖。
初次執行到synchronized代碼塊的時候,鎖對象變成偏向鎖(通過CAS修改對象頭里的鎖標志位),字面意思是“偏向於第一個獲得它的線程”的鎖。執行完同步代碼塊后,線程並不會主動釋放偏向鎖。當第二次到達同步代碼塊時,線程會判斷此時持有鎖的線程是否就是自己(持有鎖的線程ID也在對象頭里),如果是則正常往下執行。由於之前沒有釋放鎖,這里也就不需要重新加鎖。如果自始至終使用鎖的線程只有一個,很明顯偏向鎖幾乎沒有額外開銷,性能極高。
一旦有第二個線程加入鎖競爭,偏向鎖就升級為輕量級鎖(自旋鎖)。這里要明確一下什么是鎖競爭:如果多個線程輪流獲取一個鎖,但是每次獲取鎖的時候都很順利,沒有發生阻塞,那么就不存在鎖競爭。只有當某線程嘗試獲取鎖的時候,發現該鎖已經被占用,只能等待其釋放,這才發生了鎖競爭。
在輕量級鎖狀態下繼續鎖競爭,沒有搶到鎖的線程將自旋,即不停地循環判斷鎖是否能夠被成功獲取。獲取鎖的操作,其實就是通過CAS修改對象頭里的鎖標志位。先比較當前鎖標志位是否為“釋放”,如果是則將其設置為“鎖定”,比較並設置是原子性發生的。這就算搶到鎖了,然后線程將當前鎖的持有者信息修改為自己。
長時間的自旋操作是非常消耗資源的,一個線程持有鎖,其他線程就只能在原地空耗CPU,執行不了任何有效的任務,這種現象叫做忙等(busy-waiting)。如果多個線程用一個鎖,但是沒有發生鎖競爭,或者發生了很輕微的鎖競爭,那么synchronized就用輕量級鎖,允許短時間的忙等現象。這是一種折衷的想法,短時間的忙等,換取線程在用戶態和內核態之間切換的開銷。
顯然,此忙等是有限度的(有個計數器記錄自旋次數,默認允許循環10次,可以通過虛擬機參數更改)。如果鎖競爭情況嚴重,某個達到最大自旋次數的線程,會將輕量級鎖升級為重量級鎖(依然是CAS修改鎖標志位,但不修改持有鎖的線程ID)。當后續線程嘗試獲取鎖時,發現被占用的鎖是重量級鎖,則直接將自己掛起(而不是忙等),等待將來被喚醒。在JDK1.6之前,synchronized直接加重量級鎖,很明顯現在得到了很好的優化。
一個鎖只能按照 偏向鎖、輕量級鎖、重量級鎖的順序逐漸升級(也有叫鎖膨脹的),不允許降級。
感謝評論區 酷帥俊靚美的問題:
偏向鎖的一個特性是,持有鎖的線程在執行完同步代碼塊時不會釋放鎖。那么當第二個線程執行到這個synchronized代碼塊時是否一定會發生鎖競爭然后升級為輕量級鎖呢?
線程A第一次執行完同步代碼塊后,當線程B嘗試獲取鎖的時候,發現是偏向鎖,會判斷線程A是否仍然存在。如果線程A仍然存在,將線程A掛起,此時偏向鎖升級為輕量級鎖,之后線程A繼續執行,線程B自旋。但是如果判斷結果是線程A不存在了,則線程B持有此偏向鎖,鎖不升級。
五、可重入鎖(遞歸鎖)
可重入鎖的字面意思是“可以重新進入的鎖”,即允許同一個線程多次獲取同一把鎖。比如一個遞歸函數里有加鎖操作,遞歸過程中這個鎖會阻塞自己嗎?如果不會,那么這個鎖就是可重入鎖(因為這個原因可重入鎖也叫做遞歸鎖)。
Java里只要以Reentrant開頭命名的鎖都是可重入鎖,而且JDK提供的所有現成的Lock實現類,包括synchronized關鍵字鎖都是可重入的。如果你需要不可重入鎖,只能自己去實現了。網上不可重入鎖的實現真的很多,就不在這里貼代碼了。99%的業務場景用可重入鎖就可以了,剩下的1%是什么呢?我也不知道,誰可以在評論里告訴我?
JDK提供的Lock的實現類都是可重入的
六、公平鎖、非公平鎖
如果多個線程申請一把公平鎖,那么當鎖釋放的時候,先申請的先得到,非常公平。顯然如果是非公平鎖,后申請的線程可能先獲取到鎖,是隨機或者按照其他優先級排序的。
對ReentrantLock類而言,通過構造函數傳參可以指定該鎖是否是公平鎖,默認是非公平鎖。一般情況下,非公平鎖的吞吐量比公平鎖大,如果沒有特殊要求,優先使用非公平鎖。
ReentrantLock構造器可以指定為公平或非公平
對於synchronized而言,它也是一種非公平鎖,但是並沒有任何辦法使其變成公平鎖。
七、可中斷鎖
可中斷鎖,字面意思是“可以響應中斷的鎖”。
這里的關鍵是理解什么是中斷。Java並沒有提供任何直接中斷某線程的方法,只提供了中斷機制。何謂“中斷機制”?線程A向線程B發出“請你停止運行”的請求(線程B也可以自己給自己發送此請求),但線程B並不會立刻停止運行,而是自行選擇合適的時機以自己的方式響應中斷,也可以直接忽略此中斷。也就是說,Java的中斷不能直接終止線程,而是需要被中斷的線程自己決定怎么處理。這好比是父母叮囑在外的子女要注意身體,但子女是否注意身體,怎么注意身體則完全取決於自己。[2]
回到鎖的話題上來,如果線程A持有鎖,線程B等待獲取該鎖。由於線程A持有鎖的時間過長,線程B不想繼續等待了,我們可以讓線程B中斷自己或者在別的線程里中斷它,這種就是可中斷鎖。
在Java中,synchronized就是不可中斷鎖,而Lock的實現類都是可中斷鎖,可以簡單看下Lock接口。
/* Lock接口 */ public interface Lock { void lock(); // 拿不到鎖就一直等,拿到馬上返回。 void lockInterruptibly() throws InterruptedException; // 拿不到鎖就一直等,如果等待時收到中斷請求,則需要處理InterruptedException。 boolean tryLock(); // 無論拿不拿得到鎖,都馬上返回。拿到返回true,拿不到返回false。 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 同上,可以自定義等待的時間。 void unlock(); Condition newCondition(); }
八、讀寫鎖、共享鎖、互斥鎖
讀寫鎖其實是一對鎖,一個讀鎖(共享鎖)和一個寫鎖(互斥鎖、排他鎖)。
看下Java里的ReadWriteLock接口,它只規定了兩個方法,一個返回讀鎖,一個返回寫鎖。
記得之前的樂觀鎖策略嗎?所有線程隨時都可以讀,僅在寫之前判斷值有沒有被更改。
讀寫鎖其實做的事情是一樣的,但是策略稍有不同。很多情況下,線程知道自己讀取數據后,是否是為了更新它。那么何不在加鎖的時候直接明確這一點呢?如果我讀取值是為了更新它(SQL的for update就是這個意思),那么加鎖的時候就直接加寫鎖,我持有寫鎖的時候別的線程無論讀還是寫都需要等待;如果我讀取數據僅為了前端展示,那么加鎖時就明確地加一個讀鎖,其他線程如果也要加讀鎖,不需要等待,可以直接獲取(讀鎖計數器+1)。
雖然讀寫鎖感覺與樂觀鎖有點像,但是讀寫鎖是悲觀鎖策略。因為讀寫鎖並沒有在更新前判斷值有沒有被修改過,而是在加鎖前決定應該用讀鎖還是寫鎖。樂觀鎖特指無鎖編程,如果仍有疑惑可以再回到第一、二小節,看一下什么是“樂觀鎖”。
JDK提供的唯一一個ReadWriteLock接口實現類是ReentrantReadWriteLock。看名字就知道,它不僅提供了讀寫鎖,而是都是可重入鎖。 除了兩個接口方法以外,ReentrantReadWriteLock還提供了一些便於外界監控其內部工作狀態的方法,這里就不一一展開。
九、回到悲觀鎖和樂觀鎖
這篇文章經歷過一次修改,我之前認為偏向鎖和輕量級鎖是樂觀鎖,重量級鎖和Lock實現類為悲觀鎖,網上很多資料對這些概念的表述也很模糊,各執一詞。
先拋出我的結論:
我們在Java里使用的各種鎖,幾乎全都是悲觀鎖。synchronized從偏向鎖、輕量級鎖到重量級鎖,全是悲觀鎖。JDK提供的Lock實現類全是悲觀鎖。其實只要有“鎖對象”出現,那么就一定是悲觀鎖。因為樂觀鎖不是鎖,而是一個在循環里嘗試CAS的算法。
那JDK並發包里到底有沒有樂觀鎖呢?
有。java.util.concurrent.atomic包里面的原子類都是利用樂觀鎖實現的。
原子類AtomicInteger的自增方法為樂觀鎖策略
為什么網上有些資料認為偏向鎖、輕量級鎖是樂觀鎖?理由是它們底層用到了CAS?或者是把“樂觀/悲觀”與“輕量/重量”搞混了?其實,線程在搶占這些鎖的時候,確實是循環+CAS的操作,感覺好像是樂觀鎖。但問題的關鍵是,我們說一個鎖是悲觀鎖還是樂觀鎖,總是應該站在應用層,看它們是如何鎖住應用數據的,而不是站在底層看搶占鎖的過程。如果一個線程嘗試獲取鎖時,發現已經被占用,它是否繼續讀取數據,等后續要更新時再決定要不要重試?對於偏向鎖、輕量級鎖來說,顯然答案是否定的。無論是掛起還是忙等,對應用數據的讀取操作都被“擋住”了。從這個角度看,它們確實是悲觀鎖。
退一步講,也沒有必要在這些術語上狠鑽牛角尖,最重要的是理解它們的運行機制。想寫得盡量簡單一些,卻發現洋洋灑灑近萬字,只講了個皮毛。深知自己水平有限,不敢保證完全正確,只能說路漫漫其修遠兮,望指正。
文章引用自知乎:https://zhuanlan.zhihu.com/p/71156910