多執行緒知識梳理(2) synchronized 三部曲之基本使用

澤毛發表於2017-12-21

一、為什麼要使用 synchronized

使用synchronized的原因在於:它能夠確保多個執行緒在同一時刻,只能有一個執行緒處於方法或者同步塊中,它保證了執行緒對變數訪問的可見性和排他性。

二、synchronized 原理

JDK 1.6之前,synchronized的實現是基於物件上的監視器,這也被稱為重量鎖。預設情況下,每一個物件都有一個關聯的Monitor,而每個Monitor包含了一個EntryCount計數器,它是synchronized實現可重入的關鍵。

JDK 1.6之後,對鎖進行了一系列優化的措施,通過引入自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。

這些優化措施最終的目的是減少鎖操作的開銷,然而它所改變的只是鎖的實現方式,但是加鎖和解鎖這一基本原則是沒有改變的。這篇文章主要是介紹synchronized的使用,因此,在後面的介紹中,我們還是按照比較容易理解的重量鎖的方式進行分析,在之後的文章中,我們再來談一下優化後的實現策略。

2.1 進入同步方法或者程式碼塊

當一個執行緒執行某個物件的同步方法或者程式碼塊時,會先檢查這個物件所關聯的Monitor's EntryCount是否為0

  • 如果EntryCount0,那麼該執行緒就會將Monitor’s EntryCount設定為1,併成為該Monitor的所有者,接著執行該方法或者程式碼塊中的語句。
  • 如果EntryCount不為0,這時會去檢查物件所關聯的Monitor的持有者是哪一個執行緒:
  • 第一種情況:持有該Monitor的執行緒就是當前正在嘗試獲取Monitor的執行緒,那麼將EntryCount的數值加1,繼續執行方法或者程式碼塊中的語句。
  • 第二種情況:持有該Monitor的是其它的執行緒,那麼該執行緒進入阻塞狀態,直到EntryCount的數值變為0

2.2 退出同步方法或者程式碼塊

當一個執行緒從同步方法或者程式碼塊退出時,會將EntryCount1,如果EntryCount變為0,那麼該執行緒會釋放它所持有的Monitor。之前那些阻塞在synchronized的執行緒會嘗試去獲取Monitor,成功獲取Monitor的執行緒可以進入同步方法或者程式碼塊。

三、synchronized 使用

對於synchronized的使用,我們有兩種分類方法:

  • 根據使用場景分類
  • 根據Monitor關聯的物件分類。

3.1 根據使用場景分類

很多介紹synchronized的文章,都是通過使用場景進行分類的,一般來說可以分為如下四種使用場景,而每種場景下根據Monitor所關聯的物件不同,又會衍生出另外的用法:

  • 靜態方法
    //靜態方法,使用的是Class類鎖
    synchronized public static void staticMethod() {}
複製程式碼
  • 靜態方法程式碼塊
    private static final byte[] mStaticLockByte = new byte[1];

    //靜態方法程式碼塊1,使用的是Class類鎖
    public static void staticBlock1() {
        synchronized (SynchronizedObject.class) {}
    }

    //靜態方法程式碼塊2,使用的是內部靜態變數鎖
    public static void staticBlock2() {
        synchronized (mStaticLockByte) {} 
    }
複製程式碼
  • 普通方法
    //普通方法,使用的是呼叫該方法的物件鎖
    synchronized public void method() {}
複製程式碼
  • 普通方法程式碼塊
    private static final byte[] mStaticLockByte = new byte[1];
    private final byte[] mLockByte = new byte[1];

    //普通方法程式碼塊1,使用的是Class類鎖
    public void block1() {
        synchronized (SynchronizedObject.class) {}
    }

    //普通方法程式碼塊2,使用的是mLockByte的變數鎖
    public void block2() {
        synchronized (mLockByte) {} //變數需要宣告為final
    }
    
    //普通方法程式碼塊3,使用的是mStaticLockByte的變數鎖
    public void block3() {
        synchronized (mStaticLockByte) {} 
    }

    //普通方法程式碼塊4,使用的是呼叫該方法的物件鎖
    public void block4() {
        synchronized (this) {}
    }
複製程式碼

3.2 根據 Monitor 關聯的物件分類

根據使用場景進行分類,主要是為了讓大家知道如何使用synchronized關鍵字,然而要真正地理解synchronized,就需要結合第二節談到的synchronized原理,其實3.1中談到的多種場景,都是和Monitor有關,那麼從和Monitor關聯的物件來看,我們重新對3.1中的8種場景重新進行分類:

  • Class物件:
  • 靜態方法
  • 靜態方法程式碼塊1 - SynchronizedObject.class
  • 普通方法程式碼塊1 - SynchronizedObject.class
  • 呼叫方法的物件
  • 普通方法
  • 普通方法程式碼塊4 - this
  • 靜態物件
  • 靜態方法程式碼塊2 - mStaticLockByte
  • 普通方法程式碼塊3 - mStaticLockByte
  • 非靜態物件
  • 普通方法程式碼塊1 - mLockByte

如果使用場景屬於上面的同一個分類當中,那麼才有可能產生執行緒阻塞在synchronized關鍵字的情況,舉一個例子,如果A執行緒通過靜態方法訪問(分類一)並且沒有從該方法退出:

  • 這時B執行緒是通過一個物件的普通方法來訪問(分類二),那麼是不會阻塞的,這是因為呼叫該方法的物件所關聯的Monitor沒有被持有。
  • 如果B執行緒使用的是靜態方法程式碼塊來訪問,而該靜態方法程式碼塊使用的是SynchronizedObject.class來修飾(分類一),由於這兩種使用場景是屬於同一個分類,那麼就會B執行緒就會進入阻塞狀態,這是因為SynchronizedObject類所關聯的Monitor已經被A執行緒持有了。

四、小結

從表面上來看,synchronized的使用可以簡單地分為同步方法和同步程式碼塊,但是究竟在什麼情況下會導致一個執行緒在synchronized上阻塞,則需要分析synchronized方法所嘗試獲取的Monitor的是否已經被其它執行緒持有了。

相關文章