【Java 併發】好多鎖啊!偏向鎖、輕量級鎖、重量級鎖、自旋鎖、樂觀鎖、悲觀鎖 ......

相信你和我一樣,一開始學習這些鎖的時候暈頭轉向,各種各樣鎖層出不窮,所以在我學習了這些知識後,特意來總結自己的學習記錄,供大家參考學習。今天講解:偏向鎖、輕量級鎖、重量級鎖、自旋鎖、樂觀鎖、悲觀鎖、公平鎖、非公平鎖、讀寫鎖、可重入鎖

一、鎖優化

在JDK1.5 - JDK1.6中對高效併發有了很大的改進,其中一個重要的改進就是優化了很多鎖相關的技術。包括適應性自旋、縮小出、鎖粗化、輕量級鎖和偏向鎖等,都是爲了更加高效的解決併發問題。

1. 自旋鎖🔒

來源
對於之前使用的互斥同步方式進行加鎖,每一次線程競爭失敗時,都會進行阻塞,獲取到鎖的線程執行結束後又會喚醒阻塞的線程。這個線程阻塞喚醒的行爲每次都是需要轉到內核態來進行的,這會給操作系統帶來很大壓力。對於一些在同步代碼塊中執行很快的代碼就需要頻繁的阻塞喚醒線程,爲了執行很短時間的代碼而將線程阻塞喚醒很不值得,於是自旋鎖就出現了。
自旋鎖
自旋鎖在實現上就是一種“我先啥也不幹等等鎖”的態度,首先競爭鎖,如果沒有競爭到,就自己進行循環直到槍鎖成功,可以看作時以下代碼:

while (搶鎖(lock) == 失敗) {}

自旋鎖避免了線程切換的開銷,但它需要佔據處理器的時間。所以,如果鎖被佔用的時間較短,可以使用自旋鎖,反之,如果被佔用的時間很長,只會讓自旋空轉白白消耗處理器資源,帶來性能上的浪費。
因此,自旋等待也是有限度的,如果時間過長都沒獲得鎖就應該將線程掛起了。自旋的默認次數是10次

2. 自適應自旋🔒

本質上還是自旋,是自旋的 “ 加強版 ”。自適應自旋使得自旋的時間不再固定,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者來決定。也就是說,如果在同一個鎖對象中,自旋等待剛成功過,那這一次也很可能會成功,那可以適當的增加自旋的時間,比如加到100次,反之如果對於某一個鎖而言,自旋很少成功過,那以後獲取這個鎖將有可能直接省去自旋的過程,以避免浪費處理器。
可以看出,有了自適應自旋,虛擬機對程序鎖狀況的預測就會越來越準確了,也提高了性能效率。

3.鎖消除

鎖消除 指的是虛擬機編譯器運行時,雖然一些代碼上有同步的要求,但檢測到對於這些同步代碼的共享數據不存在競爭,那麼虛擬機就會將這個同步代碼塊的鎖 “消除”,也就是這個線程不需要競爭鎖,而可以直接進如代碼塊執行。這也是虛擬機對鎖的一種優化,使得併發更加高效。

4. 鎖粗化

我們在寫代碼時會被推薦使用細粒度鎖,就是將同步塊的作用範圍限制的儘量小,這樣是爲了使得需要同步的操作數量儘可能變小,就算有線程競爭,每一個線程也可以儘快拿到鎖。
鎖粗化則恰恰相反,它是將加鎖的區域放大。大多數場景都是使用更細粒度的鎖,而鎖粗化的場景就是:當有一系列的連續操作都是對於同一個對象反覆的加鎖解鎖,甚至是加到循環體內的鎖,這樣的話即使沒有線程之間的競爭,頻繁的進行互斥同步操作也會導致不必要的性能損耗。
此時,就會使用鎖粗化,虛擬機探測到上述情況,就會把加鎖同步的範圍擴展到整個操作的外部,這樣只需要加一次鎖就好了。

二、從無鎖到重量級鎖

JDK1.6後爲了減少獲得鎖和釋放鎖帶來的性能損耗,就引入了 偏向鎖 和 輕量級鎖。鎖的狀態一共有四種,級別從高到低依次是無鎖—> 偏向鎖 —> 輕量級鎖 —> 重量級鎖,這幾個狀態會隨着競爭的情況逐漸升級,注意!! 鎖可以升級,不可以降級,以下的鎖也是synchronized關鍵字優化後的鎖狀態。

1. 對象頭

在瞭解這些鎖之前,先來認識下對象頭。對於每一個對象,都有一個對象頭來保存這個對象的相關信息。一個非數組類型的對象頭中會有兩種信息:

  1. Mark Word —— 存儲對象鎖信息和hashCode等信息
  2. Class Metadata Address —— 存儲到對象類型數據的指針

所以,對於鎖的信息,就要要研究 Mark Word 相關信息,以下是32位系統的Mark Word默認存儲結構:

鎖狀態 25bit 4bit 1bit是否是偏向鎖 2bit鎖標誌位
無鎖狀態 對象的hashCode 對象分代年齡 0 01

其中 ,鎖標誌位就是表示上述的鎖的情況:

輕量級鎖 無鎖(偏向鎖) 重量級鎖 GC標記
00 01 10 11

那對於無鎖和偏向鎖的區別就是使用Mark Word中的1bit來表示了:如果爲0 就是無鎖狀態,如果爲1 就是偏向鎖狀態。

2. 偏向鎖🔒

來源
經過研究發現,大多數情況下,鎖不僅不存在多線程之間的競爭,而且還總是由同一個線程多次獲得,所以在這種情況下,爲了降低線程獲得鎖的代價,引入了偏向鎖的概念。也就是偏向鎖是用於沒有競爭的情況下的同步操作。

偏向鎖的獲得

當鎖對象第一次被線程獲取的時候,虛擬機將對象頭中的標誌位設置爲“01”,同時使用CAS操作把獲取到這個線程的ID記錄在對象的Mark Word中如果CAS成功,則線程持有偏向鎖,以後再次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作。

依次,對於一個線程進入同步代碼塊後:首先會測試以下對象頭中的Mark Word裏是否存儲着指向當前線程的偏向鎖。

  1. 此時如果測試成功,表示線程已經獲得了鎖,執行同步代碼
  2. 如果測試失敗,則再看看對象頭中表示 是否偏向鎖的標誌位 是否爲1(是否偏向)
    (1)如果還不是可偏向,那就可以使用CAS競爭獲得鎖
    (2)如果已經是偏向鎖,那說明出現了線程之間的競爭。此時可能就要根據另外線程的情況,可能是重新偏向,也有可能是做偏向撤銷,但大部分情況下就是升級成輕量級鎖了。

偏向鎖的撤銷
偏向鎖會等到競爭出現才釋放鎖,當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放
偏向鎖的撤銷首先會等到全局安全點(沒有字節碼執行)才執行:

  1. 首先暫停擁有偏向鎖的線程,檢查線程是否“活着”
  2. 如果線程已經不活動了,就將線程設置爲“無鎖狀態”
  3. 如果線程還活着,遍歷偏向對象的所記錄,棧中的所記錄和對象頭的Mark Word 要麼重新偏向於其它線程,要麼恢復到無鎖狀態或不適合偏向鎖狀態
  4. 喚醒所有暫停的線程

關閉偏向鎖
如果確認程序中所有的鎖通常情況下都處於競爭狀態,就可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking = false,系統就會進入輕量級鎖狀態。

(圖源自《Java併發編程的藝術》)
在這裏插入圖片描述

3.輕量級鎖🔒

加鎖
鎖撤銷升級爲輕量級鎖之後,那麼對象的Mark Word也會進行相應的的變化。線程執行同步塊時,過程如下:

  1. 線程在自己的棧楨中創建鎖記錄 LockRecord。並將鎖對象的對象頭中Mark Word複製到線程的剛剛創建的鎖記錄中。(官方稱之爲 Displaced Mark Word)
  2. 使用CAS將鎖對象的對象頭的MarkWord替換爲指向鎖記錄的指針。將鎖記錄中的Owner指針指向鎖對象。

如果上面操作成功,則當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程就嘗試使用自旋來獲取鎖。
(圖來源於《深入理解Java虛擬機》)
在這裏插入圖片描述
解鎖
解鎖時,會使用CAS將Displaced Mark Word 替換回對象頭,如果成功表示沒有競爭發生,反之,如果失敗,表示就會由競爭發生,鎖就會膨脹爲重量級鎖。
(圖源自《Java併發編程的藝術》)
在這裏插入圖片描述

4.重量級鎖🔒

因爲自旋會消耗CPU,所以爲了避免無用的自旋每一單鎖升級爲重量級鎖,就不會恢復到輕量級鎖。在重量級鎖的狀態下,其它線程試圖獲取鎖時就會被阻塞住,等到持有鎖的線程釋放鎖後纔會喚醒這些線程,被喚醒的線程就會繼續競爭鎖。
重量級鎖也稱爲互斥鎖、悲觀鎖

所以,Synchronized關鍵字其實在優化後,也變得效率高起來了,會逐漸從偏向鎖到輕量級鎖來膨脹。

5.鎖的優缺點

優點 缺點 使用場景
偏向鎖 加鎖和解鎖不需要額外的資源消耗,執行同步方法很快 如果線程之間有競爭,會有額外的鎖撤銷消耗 適用於只有一個線程訪問同步代碼塊的場景
輕量級鎖 競爭線程不會阻塞,提高了程序相應程度 如果始終得不到鎖,競爭的線程會自旋消耗CPU 追求響應時間,同步塊執行的時間很快
重量級鎖 線程競爭不適用自旋,不消耗CPU 線程阻塞,響應時間緩慢 追求吞吐量,同步塊執行時間較長

三、常見鎖策略

以下介紹的都是一些鎖的策略,基於這些策略下也實現了一些其它鎖。

1.樂觀鎖🔒

樂觀鎖 假設認爲數據一般情況下不會產生併發衝突,所以在數據進行提交更新的時候,纔會正式對數據是否產生併發衝突進行檢測,如果發現併發衝突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。之前講到的CAS就屬於樂觀鎖。

2. 悲觀鎖🔒

悲觀鎖 總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。Synchronized關鍵字和ReentrantLock都是一種悲觀鎖

3. 讀寫鎖🔒

多線程之間,數據的讀取方之間不會產生線程安全問題,但數據的寫入方互相之間以及和讀者之間都需要進行互斥。如果兩種場景下都用同一個鎖,就會產生極大的性能損耗。所以讀寫鎖因此而產生。
讀寫鎖(readers-writer lock):看英文可以顧名思義,在執行加鎖操作時需要額外表明讀寫意圖,讀讀之間並不互斥,而讀寫和寫寫都要求與任何人互斥。

4. 可重入鎖🔒

可重入鎖 :的字面意思是“可以重新進入的鎖”,即允許同一個線程多次獲取同一把鎖。比如一個遞歸函數裏有加鎖操作,遞歸過程中這個鎖會阻塞自己嗎?如果不會,那麼這個鎖就是可重入鎖(因爲這個原因可重入鎖也叫做遞歸鎖)。
Java裏只要以Reentrant開頭命名的鎖都是可重入鎖,而且JDK提供的所有現成的Lock實現類,包括synchronized關鍵字鎖都是可重入的

5. 公平鎖🔒

公平鎖 :多個線程按照申請鎖的順序去獲得鎖,線程會直接進入隊列去排隊,永遠都是隊列的第一位才能得到鎖。

優點:所有的線程都能得到資源,不會“餓死”在隊列中。
缺點:吞吐量會下降很多,隊列裏面除了第一個線程,其他的線程都會阻塞,cpu喚醒阻塞線程的開銷會很大。

6. 非公平鎖🔒

非公平鎖:多個線程去獲取鎖的時候,會直接去嘗試獲取,獲取不到,再去進入等待隊列,如果能獲取到,就直接獲取到鎖。不需要按照順序獲得。

優點:減少CPU喚醒線程的數量以及開銷,提高吞吐量
缺點:隊列中有的線程可能一直獲取不到鎖或者長時間獲取不到鎖,導致“餓死”。

嘮嘮叨叨:
這一次的文章有各種各樣不同的鎖,希望今天自己將這些不同的鎖講明白些了,當然這些鎖也相應的也有延申出來的各種各樣的知識,以後會逐漸都寫出來。文章參考了《深入理解Java虛擬機》以及《Java併發編程的藝術》都是很好的書,大家也可以去看看,當然本人的知識深度廣度都有限,文章如果有什麼問題歡迎大家提出指正,以後一定會繼續寫各種各樣學到的知識,歡迎點贊關注一起進步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章