網上關於Java中鎖的話題能夠說資料至關豐富,但相關內容總感受是一大串術語的羅列,讓人云裏霧裏,讀完就忘。本文但願能爲Java新人作一篇通俗易懂的整合,旨在消除對各類各樣鎖的術語的恐懼感,對每種鎖的底層實現淺嘗輒止,可是在須要時可以知道去查什麼。前端
首先要打消一種想法,就是一個鎖只能屬於一種分類。其實並非這樣,好比一個鎖能夠同時是悲觀鎖、可重入鎖、公平鎖、可中斷鎖等等,就像一我的能夠是男人、醫生、健身愛好者、遊戲玩家,這並不矛盾。OK,國際慣例,上乾貨。java
Java中有兩種加鎖的方式:一種是用synchronized關鍵字,另外一種是用Lock接口的實現類。算法
形象地說,synchronized關鍵字是自動檔,能夠知足一切平常駕駛需求。可是若是你想要玩漂移或者各類騷操做,就須要手動檔了——各類Lock的實現類。編程
因此若是你只是想要簡單的加個鎖,對性能也沒什麼特別的要求,用synchronized關鍵字就足夠了。自Java 5以後,纔在java.util.concurrent.locks包下有了另一種方式來實現鎖,那就是Lock。也就是說,synchronized是Java語言內置的關鍵字,而Lock是一個接口,這個接口的實現類在代碼層面實現了鎖的功能,具體細節不在本文展開,有興趣能夠研究下AbstractQueuedSynchronizer類,寫得能夠說是牛逼爆了。bash
ReentrantLock、ReadLock、WriteLock 是Lock接口最重要的三個實現類。對應了「可重入鎖」、「讀鎖」和「寫鎖」,後面會講它們的用途。併發
ReadWriteLock實際上是一個工廠接口,而ReentrantReadWriteLock是ReadWriteLock的實現類,它包含兩個靜態內部類ReadLock和WriteLock。這兩個靜態內部類又分別實現了Lock接口。函數
咱們中止深究源碼,僅從使用的角度看,Lock與synchronized的區別是什麼?在接下來的幾個小節中,我將梳理各類鎖分類的概念,以及synchronized關鍵字、各類Lock實現類之間的區別與聯繫。性能
鎖的一種宏觀分類方式是悲觀鎖和樂觀鎖。悲觀鎖與樂觀鎖並非特指某個鎖(Java中沒有哪一個Lock實現類就叫PessimisticLock或OptimisticLock),而是在併發狀況下的兩種不一樣策略。學習
悲觀鎖(Pessimistic Lock), 就是很悲觀,每次去拿數據的時候都認爲別人會修改。因此每次在拿數據的時候都會上鎖。這樣別人想拿數據就被擋住,直到悲觀鎖被釋放。優化
樂觀鎖(Optimistic Lock), 就是很樂觀,每次去拿數據的時候都認爲別人不會修改。因此不會上鎖,不會上鎖!可是若是想要更新數據,則會在更新前檢查在讀取至更新這段時間別人有沒有修改過這個數據。若是修改過,則從新讀取,再次嘗試更新,循環上述步驟直到更新成功(固然也容許更新失敗的線程放棄操做)。
悲觀鎖阻塞事務,樂觀鎖回滾重試,它們各有優缺點,不要認爲一種必定好於另外一種。像樂觀鎖適用於寫比較少的狀況下,即衝突真的不多發生的時候,這樣能夠省去鎖的開銷,加大了系統的整個吞吐量。但若是常常產生衝突,上層應用會不斷的進行重試,這樣反卻是下降了性能,因此這種狀況下用悲觀鎖就比較合適。
說到樂觀鎖,就必須提到一個概念:CAS
什麼是CAS呢?Compare-and-Swap,即比較並替換,也有叫作Compare-and-Set的,比較並設置。
一、比較:讀取到了一個值A,在將其更新爲B以前,檢查原值是否仍爲A(未被其餘線程改動)。
二、設置:若是是,將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指令,從硬件層面保證了操做的原子性,以達到相似於鎖的效果。
由於整個過程當中並無「加鎖」和「解鎖」操做,所以樂觀鎖策略也被稱爲無鎖編程。換句話說,樂觀鎖其實不是「鎖」,它僅僅是一個循環重試CAS的算法而已!
有一種鎖叫自旋鎖。所謂自旋,說白了就是一個 while(true) 無限循環。
剛剛的樂觀鎖就有相似的無限循環操做,那麼它是自旋鎖嗎?
感謝評論區 養貓的蝦的指正。
不是。儘管自旋與 while(true) 的操做是同樣的,但仍是應該將這兩個術語分開。「自旋」這兩個字,特指自旋鎖的自旋。
然而在JDK中並無自旋鎖(SpinLock)這個類,那什麼纔是自旋鎖呢?讀完下個小節就知道了。
前面提到,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%是什麼呢?我也不知道,誰能夠在評論裏告訴我?
若是多個線程申請一把公平鎖,那麼當鎖釋放的時候,先申請的先獲得,很是公平。顯然若是是非公平鎖,後申請的線程可能先獲取到鎖,是隨機或者按照其餘優先級排序的。
對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)。
雖然讀寫鎖感受與樂觀鎖有點像,可是讀寫鎖是悲觀鎖策略。由於讀寫鎖並無在更新前判斷值有沒有被修改過,而是在加鎖前決定應該用讀鎖仍是寫鎖。樂觀鎖特指無鎖編程,若是仍有疑惑能夠再回到第1、二小節,看一下什麼是「樂觀鎖」。
JDK提供的惟一一個ReadWriteLock接口實現類是ReentrantReadWriteLock。看名字就知道,它不只提供了讀寫鎖,而是都是可重入鎖。 除了兩個接口方法之外,ReentrantReadWriteLock還提供了一些便於外界監控其內部工做狀態的方法,這裏就不一一展開。
這篇文章經歷過一次修改,我以前認爲偏向鎖和輕量級鎖是樂觀鎖,重量級鎖和Lock實現類爲悲觀鎖,網上不少資料對這些概念的表述也很模糊,各執一詞。
先拋出個人結論:
咱們在Java裏使用的各類鎖,幾乎全都是悲觀鎖。synchronized從偏向鎖、輕量級鎖到重量級鎖,全是悲觀鎖。JDK提供的Lock實現類全是悲觀鎖。其實只要有「鎖對象」出現,那麼就必定是悲觀鎖。由於樂觀鎖不是鎖,而是一個在循環裏嘗試CAS的算法。
那JDK併發包裏到底有沒有樂觀鎖呢?
有。java.util.concurrent.atomic包裏面的原子類都是利用樂觀鎖實現的。
爲何網上有些資料認爲偏向鎖、輕量級鎖是樂觀鎖?理由是它們底層用到了CAS?或者是把「樂觀/悲觀」與「輕量/重量」搞混了?其實,線程在搶佔這些鎖的時候,確實是循環+CAS的操做,感受好像是樂觀鎖。但問題的關鍵是,咱們說一個鎖是悲觀鎖仍是樂觀鎖,老是應該站在應用層,看它們是如何鎖住應用數據的,而不是站在底層看搶佔鎖的過程。若是一個線程嘗試獲取鎖時,發現已經被佔用,它是否繼續讀取數據,等後續要更新時再決定要不要重試?對於偏向鎖、輕量級鎖來講,顯然答案是否認的。不管是掛起仍是忙等,對應用數據的讀取操做都被「擋住」了。從這個角度看,它們確實是悲觀鎖。
退一步講,也沒有必要在這些術語上狠鑽牛角尖,最重要的是理解它們的運行機制。想寫得儘可能簡單一些,卻發現洋洋灑灑近萬字,只講了個皮毛。深知本身水平有限,不敢保證徹底正確,只能說路漫漫其修遠兮,望指正。