Java synchronized-鎖的概念

目錄

 

synchronized的用法

關於鎖

synchronized鎖的升級

偏向鎖

基本原理

偏向鎖的獲取和撤銷邏輯

偏向鎖的撤銷

輕量級鎖

自旋鎖

輕量級鎖的解鎖

重量級鎖

synchronized與wait、notify、notifyAll

wait、notify、notifyAll 基本概念


synchronized的用法

1.修飾實例方法,作用於當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。此時鎖對象爲當前對象。

2.修飾靜態方法,作用於當前類對象的鎖,進入同步代碼前要獲得當前類對象的鎖。此時鎖對象爲類對象。

3.修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼塊之前要獲得給定對象的鎖。鎖對象被指定。

值得注意的是,當一個同步方法的鎖沒有被線程持有的時候,一個持有不同鎖的線程能夠該同步方法塊,並替換自己的鎖對象。

關於鎖

.鎖是什麼?

鎖是處理併發的一種同步手段。Java提供的加鎖方法就是synchronized關鍵字。

.爲什麼任何對象都可以實現鎖?

1.首先,Java中的每個對象都派生自Object類。並且每一個Java Object在JVM內部都有一個native的C++對象oop/oopDescjin'進行對應。

2.線程獲取鎖的時候,實際上就是獲取了一個監視器對象monitor。該對象可以認爲是一個同步對象。所有的Java對象是天生攜帶monitor的。當多個線程訪問同步代碼塊時,相當於去爭搶對象監視器,修改對象中的鎖標識。當鎖的標識改變,也就自然hu會出現不同種類的鎖。

synchronized鎖的升級

爲了減少獲得鎖和釋放鎖帶來的性能消耗,jdk在不斷髮展中引入了偏向鎖,輕量級鎖的概念。因此我們會發現在synchronized中,鎖存在四種狀態。

分別是:無鎖、偏向鎖、輕量級鎖、重量級鎖

偏向鎖

基本原理

大部分情況下,鎖不僅僅不存在線程競爭,而是總是由同一個線程多次獲得。爲了讓線程獲取鎖的代價更低就引入了偏向鎖的概念。

即當一個線程訪問了加鎖的代碼塊時,會在對象頭中存儲當前線程的ID,後續這個線程進入和退出這段代碼時,不需要再次加鎖和釋放鎖。而是直接比較對象頭中是否存儲了指向當前線程的偏向鎖。

偏向鎖的獲取和撤銷邏輯

1.首先獲取鎖對象的MarkWord,MarkWord記錄鎖標識位。判斷是否處於可偏向狀態。(biased_lock=1、且ThreadID爲空)。即目標對象頭中未存儲線程ID。

2.如果是可偏向狀態,則通過CAS操作,把當前線程的ID寫入到MarkWord中。

如果CAS成功,表明當前線程已經獲得了鎖對象的偏向鎖,接着執行同步代碼

如果CAS失敗,說明有其它線程已經獲得了偏向鎖。這種情況說明當前鎖存在競爭,需要撤銷已獲得偏向鎖的線程,並且把它持有的鎖升級爲輕量級鎖。(該操作需要等到全局安全點,也就是沒有線程在執行字節碼,才能執行)

4.如果是已偏向狀態,需要檢查MarkWord中存儲的ThreadID是否等於當前線程的ThreadID

如果相等,不需要再次獲得鎖,可直接執行同步代碼塊。

如果不相等,說明當前鎖偏向於其它線程,需要撤銷偏向鎖升級到輕量級鎖。

偏向鎖的撤銷

偏向鎖的撤銷並不是把對象恢復到無鎖可偏向的狀態(因爲偏向鎖並不存在鎖釋放的概念),而是在獲取偏向鎖的過程中,發現CAS失敗也就是存在線程競爭時,直接把偏向鎖升級到了輕量級鎖的狀態。

對原持有偏向鎖的線程進行撤銷時,原獲得偏向鎖的線程有兩種情況

1.原獲得偏向鎖的線程如果已經退出了臨界區,也就是同步代碼塊執行完了,那麼這個時候會把對象頭設置成無鎖狀態並且爭搶鎖的線程可以基於CAS重新偏向當前線程。

2.如果原獲得偏向鎖的線程的同步代碼塊還沒執行完,處於臨界區內,這個時候會把原獲得偏向鎖的線程升級爲輕量級鎖後繼續執行同步代碼塊。

輕量級鎖

基本原理和加鎖解鎖邏輯

當鎖升級爲輕量級鎖之後,對象的MarkWord也會進行相應的變化。

升級爲輕量級鎖的過程

1.線程在自己的棧幀中創建鎖記錄,LockRecord

2.將鎖對象的對象頭中的MarkWord複製到線程剛剛創建的鎖記錄中。

3.將鎖記錄中的Owner指針指向鎖對象。

4.將鎖對象的對象頭的MarkWord替換爲指向鎖記錄的指針。

自旋鎖

輕量級鎖在加鎖過程中,會用到自旋鎖。

自旋鎖是指,當有另外一個線程來競爭獲得鎖時,線程就會在原地循環等待,而不是把競爭鎖的線程給阻塞。直到那個獲得鎖得線程釋放鎖之後,這個線程就可以馬上獲得鎖了。(值得注意的是,鎖在原地循環的時候是會消耗CPU的,就相當於在執行一個空的for循環)。

所以,輕量級鎖適用於那些同步代碼塊執行的很快的場景。這樣,線程原地等待很短的時間就能獲得鎖了。

默認情況下自旋的次數是10次,可以通過preBlockSpin來修改。

輕量級鎖的解鎖

輕量級鎖的釋放邏輯其實就是獲得鎖的逆向邏輯,通過CAS操作把線程棧幀中的LockRecord替換到鎖對象的MarkWord中。如果成功表示沒有競爭。如果失敗,表示當前鎖存在競爭,那麼輕量級鎖就會膨脹成爲重量級鎖。

重量級鎖

重量級鎖的基本原理。當輕量級鎖膨脹到重量級鎖之後,意味着線程只能被掛起阻塞來等待被喚醒了。

任意線程對Object的訪問,首先要獲得Object的監視器。如果獲取失敗,線程進入同步隊列,線程狀態變爲BLOCKED。當訪問Object的前驅(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的線程,使其重新嘗試對監視器的獲取。

synchronized關鍵字的底層原理

public class Syn {
    public static void main(String[] args) {
        synchronized (Syn.class) {
            System.out.println("this is a synchronized method");
        }
    }

}

一段簡單的代碼,通過JDK自帶的javap命令可以該類的相關字節碼信息。找到對應的class文件目錄。

通過執行命令  javap -c -s -v -l xxx.class 命令查看字節碼

synchronized關鍵字修飾的代碼塊,在JVM層面使用monitorenter指令和monitorexit指令對代碼塊進行加鎖和釋放鎖。其中monitorenter指令指向同步代碼塊開始的地方,monitorexit代碼塊指向同步代碼塊結束的地方。monitorexit指令有兩個的原因是,當程序執行異常的時候也是需要釋放鎖的。所以結束指令有兩個。

當線程獲取鎖的時候,實際上就是獲得了monitorenter指令的執行權,monitorenter指令由monitor對象持有。monitor存在於每個Java對象的對象頭中。這也是爲什麼Java中任意對象都可以作爲鎖的原因。存在於Mark Word中的鎖標識位,爲零的時候,表示鎖可以被獲取。鎖被獲取後,鎖計數器由0變爲1。若一個線程重複獲取同一個對象鎖,則鎖計數器遞增至獲取次數。

需要特別注意的是,當synchronized修飾方法的時候並沒有monitorenter 指令和 monitorexit 指令,取而代之的是 ACC_SYNCHRONIZED 標識

synchronized與wait、notify、notifyAll

wait、notify、notifyAll 基本概念

wait:表示持有對象鎖的線程A準備釋放對象的鎖權限,釋放CPU資源並進入等待狀態。

notify:表示持有對象鎖的線程 A 準備釋放對象鎖權限,通知 jvm 喚醒某個競爭該對象鎖的線程 X。線程 A 代碼執行結束並且釋放了鎖之後,線程 X 直接獲得對象鎖權限,其他競爭線程繼續等待

(即使線程 X 同步完畢,釋放對象鎖,其他競爭線程仍然等待,直至有新的 notify ,notifyAll 被調用)

notifyAll:notifyall 和 notify 的區別在於,notifyAll 會喚醒所有競爭同一個對象鎖的所有線程,當已經獲得鎖的線程A 釋放鎖之後,所有被喚醒的線程都有可能獲得對象鎖權限

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