synchronized 鎖的升級


synchronized 的基本認識

在多線程並發編程中 synchronized 一直是元老級角色,很 多人都會稱呼它為重量級鎖。但是,隨着 Java SE 1.6 對 synchronized 進行了各種優化之后,有些情況下它就並不 那么重,Java SE 1.6 中為了減少獲得鎖和釋放鎖帶來的性 能消耗而引入的偏向鎖和輕量級鎖。

synchronized 有三種方式來加鎖,

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

2. 靜態方法,作用於當前類對象加鎖,進入同步代碼前要 獲得當前類對象的鎖

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

    //修飾實例方法, 鎖作用域是this
    public synchronized void SyncMethod1() {

    }

    //修飾實例方法2,鎖作用域是this.lock
    public void SyncMethod2() {
        synchronized (this.lock) {
        }
    }

    //修飾實例方法3, 鎖作用域是this對象
    public synchronized void SyncMethod3() {
        synchronized (this) {
        }
    }
    //靜態方法 鎖作用域是整個class
    public static synchronized void SyncMethod4() {

    }

    //修飾代碼塊 
    public   void SyncMethod5() {
        //.... 代碼邏輯
        synchronized (this.lock){
            //同步代碼
        }
        //.... 代碼邏輯
    }

 


 

Mark word

Mark word 記錄了對象和鎖有關的信息,當某個對象被 synchronized 關鍵字當成同步鎖時,那么圍繞這個鎖的一 系列操作都和 Mark word 有關系。Mark Word 在 32 位虛 擬機的長度是 32bit、在 64 位虛擬機的長度是 64bit。 Mark Word 里面存儲的數據會隨着鎖標志位的變化而變化, Mark Word 可能變化為存儲以下 5 中情況

 

為什么任何對象都可以實現鎖

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

2. 線程在獲取鎖的時候,實際上就是獲得一個監視器對象 (monitor) ,monitor 可以認為是一個同步對象,所有的 Java 對象是天生攜帶 monitor。在 hotspot 源碼的 markOop.hpp 文件中,可以看到下面這段代碼。

多個線程訪問同步代碼塊時,相當於去爭搶對象監視器 修改對象中的鎖標識,上面的代碼中ObjectMonitor這個 對象和線程爭搶鎖的邏輯有密切的關系


 

synchronized 鎖的升級

在分析 markword 時,提到了偏向鎖、輕量級鎖、重量級 鎖。

JDK1.6 之后做了一些優化,為了減少獲得鎖和釋放鎖帶來 的性能開銷,引入了偏向鎖、輕量級鎖的概念。因此大家 會發現在 synchronized 中,鎖存在四種狀態 分別是:無鎖、偏向鎖、輕量級鎖、重量級鎖; 鎖的狀態 根據競爭激烈的程度從低到高不斷升級。

偏向鎖的基本原理

前面說過,大部分情況下,鎖不僅僅不存在多線程競爭, 而是總是由同一個線程多次獲得,為了讓線程獲取鎖的代 價更低就引入了偏向鎖的概念。怎么理解偏向鎖呢? 當一個線程訪問加了同步鎖的代碼塊時,會在對象頭中存 儲當前線程的 ID,后續這個線程進入和退出這段加了同步 鎖的代碼塊時,不需要再次加鎖和釋放鎖。而是直接比較 對象頭里面是否存儲了指向當前線程的偏向鎖。如果相等 表示偏向鎖是偏向於當前線程的,就不需要再嘗試獲得鎖 了

 偏向鎖的獲取和撤銷邏輯

1. 首先獲取鎖 對象的 Markword,判斷是否處於可偏向狀 態。(biased_lock=1、且 ThreadId 為空)

 2. 如果是可偏向狀態,則通過 CAS 操作,把當前線程的 ID 寫入到 MarkWord a) 如果 cas 成功,那么 markword 就會變成這樣。 表示已經獲得了鎖對象的偏向鎖,接着執行同步代碼 塊 b) 如果 cas 失敗,說明有其他線程已經獲得了偏向鎖, 這種情況說明當前鎖存在競爭,需要撤銷已獲得偏向 鎖的線程,並且把它持有的鎖升級為輕量級鎖(這個 操作需要等到全局安全點,也就是沒有線程在執行字 節碼)才能執行

3. 如果是已偏向狀態,需要檢查 markword 中存儲的 ThreadID 是否等於當前線程的 ThreadID a) 如果相等,不需要再次獲得鎖,可直接執行同步代碼 塊 b) 如果不相等,說明當前鎖偏向於其他線程,需要撤銷 偏向鎖並升級到輕量級鎖

 偏向鎖的撤銷

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

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

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

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

 

在我們的應用開發中,絕大部分情況下一定會存在 2 個以 上的線程競爭,那么如果開啟偏向鎖,反而會提升獲取鎖 的資源消耗。所以可以通過 jvm 參數 UseBiasedLocking 來設置開啟或關閉偏向鎖

 

輕量級鎖的基本原理

 輕量級鎖的加鎖和解鎖邏輯

 

鎖升級為輕量級鎖之后,對象的 Markword 也會進行相應 的的變化。升級為輕量級鎖的過程:

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

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

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

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

 

 

 

 自旋鎖

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

所謂自旋,就是指當有另外一個線程來競爭鎖時,這個線 程會在原地循環等待,而不是把該線程給阻塞,直到那個 獲得鎖的線程釋放鎖之后,這個線程就可以馬上獲得鎖的。

注意,鎖在原地循環的時候,是會消耗 cpu 的,就相當於 在執行一個啥也沒有的 for 循環。

所以,輕量級鎖適用於那些同步代碼塊執行的很快的場景,這樣,線程原地等待很短的時間就能夠獲得鎖了。 自旋鎖的使用,其實也是有一定的概率背景,在大部分同 步代碼塊執行的時間都是很短的。所以通過看似無異議的 循環反而能提升鎖的性能。 但是自旋必須要有一定的條件控制,否則如果一個線程執行同步代碼塊的時間很長,那么這個線程不斷的循環反而 會消耗 CPU 資源。默認情況下自旋的次數是 10 次, 可以通過 preBlockSpin 來修改

在 JDK1.6 之后,引入了自適應自旋鎖,自適應意味着自旋 的次數不是固定不變的,而是根據前一次在同一個鎖上自 旋的時間以及鎖的擁有者的狀態來決定。 如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並 且持有鎖的線程正在運行中,那么虛擬機就會認為這次自 旋也是很有可能再次成功,進而它將允許自旋等待持續相 對更長的時間。

如果對於某個鎖,自旋很少成功獲得過, 那在以后嘗試獲取這個鎖時將可能省略掉自旋過程,直接 阻塞線程,避免浪費處理器資源

輕量級鎖的解鎖

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

 

重量級鎖的基本原理

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

 

加了同步代碼塊以后,在字節碼中會看到一個 monitorenter 和 monitorexit。

每一個 JAVA 對象都會與一個監視器 monitor 關聯,我們 可以把它理解成為一把鎖,當一個線程想要執行一段被 synchronized 修飾的同步方法或者代碼塊時,該線程得先 獲取到 synchronized 修飾的對象對應的 monitor。

monitorenter 表示去獲得一個對象監視器。monitorexit 表 示釋放 monitor 監視器的所有權,使得其他被阻塞的線程 可以嘗試去獲得這個監視器 monitor 依賴操作系統的 MutexLock(互斥鎖)來實現的, 線 程被阻塞后便進入內核(Linux)調度狀態,這個會導致系 統在用戶態與內核態之間來回切換,嚴重影響鎖的性能


回顧線程的競爭機制

再來回顧一下線程的競爭機制對於鎖升級這塊的一些基本 流程。

加入有這樣一個同步代碼塊,存在 Thread#1、Thread#2 等 多個線程

情況一:只有 Thread#1 會進入臨界區;

情況二:Thread#1 和 Thread#2 交替進入臨界區,競爭不激 烈;

情況三:Thread#1/Thread#2/Thread3… 同時進入臨界區, 競爭激烈

 

偏向鎖

此時當 Thread#1 進入臨界區時,JVM 會將 lockObject 的 對象頭 Mark Word 的鎖標志位設為“01”,同時會用 CAS 操作把 Thread#1 的線程 ID 記錄到 Mark Word 中,此時進 入偏向模式。所謂“偏向”,指的是這個鎖會偏向於 Thread#1, 若接下來沒有其他線程進入臨界區,則 Thread#1 再出入 臨界區無需再執行任何同步操作。也就是說,若只有 Thread#1 會進入臨界區,實際上只有 Thread#1 初次進入 臨界區時需要執行 CAS 操作,以后再出入臨界區都不會有 同步操作帶來的開銷。

 輕量級鎖

偏向鎖的場景太過於理想化,更多的時候是 Thread#2 也 會嘗試進入臨界區, 如果 Thread#2 也進入臨界區但是 Thread#1 還沒有執行完同步代碼塊時,會暫停 Thread#1 並且升級到輕量級鎖。Thread#2 通過自旋再次嘗試以輕量 級鎖的方式來獲取鎖

重量級鎖

如果 Thread#1 和 Thread#2 正常交替執行,那么輕量級鎖 基本能夠滿足鎖的需求。但是如果 Thread#1 和 Thread#2 同時進入臨界區,那么輕量級鎖就會膨脹為重量級鎖,意 味着 Thread#1 線程獲得了重量級鎖的情況下,Thread#2 就會被阻塞

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM