Synchronized的實現原理以及優化

齊天小聖^O^發表於2020-11-09

在多執行緒學習階段,我們經常會使用synchronized來解決執行緒間的同步問題,很少關注過它的實現原理,但在併發程式設計的學習中,我們需要對synchronized的實現原理以及優化有更多的瞭解。

目錄

一、利用sychronized實現同步的三種方式

二、synchronized實現原理

三、對synchronized的優化


一、利用sychronized實現同步的三種方式

  1. 對於普通同步方法,鎖是當前例項物件this。
  2. 對於靜態同步方法,鎖是當前類的Class物件。
  3. 對於同步方法塊,鎖是Synchonized括號裡配置的物件。

二、synchronized實現原理

先來看下面一段程式碼:

public class SynchronizedTest {
    
    public static void main(String[] args) {
        
        synchronized (SynchronizedTest.class) {
            System.out.println("被鎖內容。。。");
        }
        
    }
}

這是一個被synchronized修飾的同步程式碼塊,將這段程式碼反編譯後結果如下。

可以看到反編譯的結果中出現了monitorentermonitorexit兩個指令。這個兩個指令的意義:當虛擬機器執行到monitorenter指令時就會嘗試獲取物件的監視器鎖(monitor),如果monitor的進入數為0,則該執行緒進入monitor,然後將進入數+1,該執行緒即為monitor的所有者;如果該執行緒已經是monitor的所有者了,則重新進入,monitor的進入數+1;如果已經有其他執行緒佔有了該monitor,則該執行緒進入阻塞狀態,直到monitor進入數為0,再重新嘗試獲取monitor擁有權。當虛擬機器執行到monitorexit指令時,monitor的進入數-1,如果減1後進入數為0,則執行緒退出monitor,該執行緒不再是monitor的所有者,其他被這個monitor阻塞的執行緒可以嘗試獲取monitor的所有權。

上面是被synchronized修飾的同步程式碼塊,再來看下面這段程式碼:

public class SynchronizedTest {
    
    public static synchronized void method () {
        System.out.println("被鎖內容。。。");
    }
    
    public static void main(String[] args) {
        method();
    }
}

method()是一個被synchronized修飾的靜態同步方法,同樣對這段程式碼進行反編譯後結果如下。

可以看到在反編譯結果中,method()方法的flags中出現了一個ACC_SYNCHRONIZED標識。它的意義:當方法被呼叫時,呼叫指令將會先檢查是否有ACC_SYNCHRONIZED標識,如果有,則執行緒先獲取monitor,獲取成功後再執行方法體,方法執行完後再釋放monitor,在此期間其他執行緒無法獲取同一個monitor物件。

總結:對於同步程式碼塊,在編譯前後會被編譯器生成monitorenter和monitorexit兩個指令;而對於同步方法,在編譯後會多出一個ACC_SYNCHRONIZED標識。他們的共同作用是,通過在物件頭設定標記達到獲取鎖和釋放鎖的目的。

三、對synchronized的優化

JDK1.6以後,Java對synchronized做了很多的優化。

自旋鎖:執行緒在阻塞和喚醒之間的切換所要花費的代價非常大。自旋鎖是在把執行緒進行阻塞前先讓執行緒進行自旋等待一段時間,因為很有可能在這段時間其他執行緒會釋放該執行緒所需要的鎖,這樣的話該執行緒就不用再進行阻塞操作,也避免了阻塞和喚醒之間的切換。

鎖消除:虛擬機器編譯器在執行時,對於一些程式碼上要求同步,但經過檢測(逃逸分析)發現實際上不可能存在共享資料競爭的情況,於是將鎖進行消除。如StringBuffer中的append()方法是被Synchronized修飾過的,但在很多情況下它只會在一個執行緒中被使用,如果編譯器能夠確定這個StringBuffer物件只會在一個執行緒中被使用,就代表這個執行緒一定是安全的,就會做出一定的優化,把對應的synchronized消除,省去加鎖解鎖的開銷,提高效率。

鎖粗化:增大鎖的作用域,減少頻繁對同一個程式碼塊的加鎖和解鎖操作,提升效能。比如在一個迴圈體內對同一個程式碼塊進行頻繁的加鎖和解鎖的操作,這樣會增大開銷。我們可以只用在迴圈體外加一次鎖就可以了,相當於多個小的同步程式碼塊合併成了一個大的同步程式碼塊,這樣就無需頻繁申請和釋放鎖了,減少效能上的開銷。

鎖膨脹:

  • 偏向鎖:一個物件被初始化後,如果還沒有任何執行緒來獲取它的鎖時,它就是可偏向的,當執行緒1獲取了它的鎖後,它會在物件頭的MarkWord中記錄下執行緒1的ID,之後執行緒1再次嘗試獲取這個鎖時,這個鎖發現記錄中的執行緒ID正是執行緒1,則允許執行緒1直接執行同步程式碼,開銷很小。此時執行緒2來競爭鎖,但MarkWord中的執行緒ID並未指向執行緒2,判斷當前物件頭是否為偏向鎖以及鎖是否被其他執行緒佔有,如果是偏向鎖且鎖已經不再被執行緒1佔有,則執行緒2通過CAS操作將MarkWord中的執行緒ID指向執行緒2(此時鎖不升級仍為偏向鎖);若鎖此刻仍然被執行緒1佔有,則代表此時有競爭了,到達全域性安全點後(safepoint)執行緒1被暫時掛起,此時鎖升級為輕量級鎖,被掛起的執行緒1繼續執行同步程式碼,執行緒2通過自旋方式獲取鎖。這種鎖狀態下的執行緒無實際的競爭。
  • 輕量級鎖:輕量級鎖是由偏向鎖在存在競爭的情況下升級而來。在這種情況下並不存在實際的競爭,最多也是短時間的競爭,使用CAS就可以解決。如果當前執行緒CAS競爭鎖失敗,則通過自旋的方式獲取鎖,若自旋一定次數後依然沒有獲得鎖,則升級為重量級鎖。這種鎖狀態下的執行緒無實際競爭,多個執行緒交替使用鎖,允許短時間的競爭。
  • 重量級鎖:當多個執行緒存在實際的競爭,且競爭的時間較長,偏向鎖和輕量級鎖都不再滿足需求了,此時鎖將會升級為重量級鎖。重量級鎖通過同步機制實現,會讓其他拿不到鎖的執行緒進入阻塞狀態,這種鎖的開銷較大。這種鎖狀態下的執行緒存在實際競爭,且競爭時間較長。虛擬機器優先使用偏向鎖,在有必要的情況下再逐步升級,這樣能夠有效提高了鎖的效能。

 

 

 

 

 

 

 

 

相關文章