1. 程式人生 > >原始碼閱讀:Java併發之synchronized實現原理

原始碼閱讀:Java併發之synchronized實現原理

執行緒安全是併發程式設計中的重要關注點,應該注意到的是,造成執行緒安全問題的主要誘因有兩點,一是存在共享資料(也稱臨界資源),二是存在多條執行緒共同操作共享資料。因此為了解決這個問題,我們可能需要這樣一個方案,當存在多個執行緒操作共享資料時,需要保證同一時刻有且只有一個執行緒在操作共享資料,其他執行緒必須等到該執行緒處理完資料後再進行,這種方式有個高尚的名稱叫互斥鎖,即能達到互斥訪問目的的鎖,也就是說當一個共享資料被當前正在訪問的執行緒加上互斥鎖後,在同一個時刻,其他執行緒只能處於等待的狀態,直到當前執行緒處理完畢釋放該鎖。在 Java 中,關鍵字 synchronized可以保證在同一個時刻,只有一個執行緒可以執行某個方法或者某個程式碼塊(主要是對方法或者程式碼塊中存在共享資料的操作),同時我們還應該注意到synchronized另外一個重要的作用,synchronized可保證一個執行緒的變化(主要是共享資料的變化)被其他執行緒所看到(保證可見性,完全可以替代Volatile功能),這點確實也是很重要的。

synchronized的三種應用方式

synchronized關鍵字最主要有以下3種應用方式,下面分別介紹

  • 修飾例項方法,作用於當前例項加鎖,進入同步程式碼前要獲得當前例項的鎖

  • 修飾靜態方法,作用於當前類物件加鎖,進入同步程式碼前要獲得當前類物件的鎖

  • 修飾程式碼塊,指定加鎖物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖。

synchronized作用於例項方法

所謂的例項物件鎖就是用synchronized修飾例項物件中的例項方法,注意是例項方法不包括靜態方法,如下

public class AccountingSync implements Runnable{
    //共享資源(臨界資源)
    static int i=0;

    /**
     * synchronized 修飾例項方法
     */
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    /**
     * 輸出結果:
     * 2000000
     */
}

上述程式碼中,我們開啟兩個執行緒操作同一個共享資源即變數i,由於i++;操作並不具備原子性,該操作是先讀取值,然後寫回一個新值,相當於原來的值加上1,分兩步完成,如果第二個執行緒在第一個執行緒讀取舊值和寫回新值期間讀取i的域值,那麼第二個執行緒就會與第一個執行緒一起看到同一個值,並執行相同值的加1操作,這也就造成了執行緒安全失敗,因此對於increase方法必須使用synchronized修飾,以便保證執行緒安全。此時我們應該注意到synchronized修飾的是例項方法increase,在這樣的情況下,當前執行緒的鎖便是例項物件instance,注意Java中的執行緒同步鎖可以是任意物件。從程式碼執行結果來看確實是正確的,倘若我們沒有使用synchronized

關鍵字,其最終輸出結果就很可能小於2000000,這便是synchronized關鍵字的作用。這裡我們還需要意識到,當一個執行緒正在訪問一個物件的 synchronized 例項方法,那麼其他執行緒不能訪問該物件的其他 synchronized 方法,畢竟一個物件只有一把鎖,當一個執行緒獲取了該物件的鎖之後,其他執行緒無法獲取該物件的鎖,所以無法訪問該物件的其他synchronized例項方法,但是其他執行緒還是可以訪問該例項物件的其他非synchronized方法,當然如果是一個執行緒 A 需要訪問例項物件 obj1 的 synchronized 方法 f1(當前物件鎖是obj1),另一個執行緒 B 需要訪問例項物件 obj2 的 synchronized 方法 f2(當前物件鎖是obj2),這樣是允許的,因為兩個例項物件鎖並不同相同,此時如果兩個執行緒操作資料並非共享的,執行緒安全是有保障的,遺憾的是如果兩個執行緒操作的是共享資料,那麼執行緒安全就有可能無法保證了,如下程式碼將演示出該現象

public class AccountingSyncBad implements Runnable{
    static int i=0;
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新例項
        Thread t1=new Thread(new AccountingSyncBad());
        //new新例項
        Thread t2=new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        //join含義:當前執行緒A等待thread執行緒終止之後才能從thread.join()返回
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

上述程式碼與前面不同的是我們同時建立了兩個新例項AccountingSyncBad,然後啟動兩個不同的執行緒對共享變數i進行操作,但很遺憾操作結果是1452317而不是期望結果2000000,因為上述程式碼犯了嚴重的錯誤,雖然我們使用synchronized修飾了increase方法,但卻new了兩個不同的例項物件,這也就意味著存在著兩個不同的例項物件鎖,因此t1和t2都會進入各自的物件鎖,也就是說t1和t2執行緒使用的是不同的鎖,因此執行緒安全是無法保證的。解決這種困境的的方式是將synchronized作用於靜態的increase方法,這樣的話,物件鎖就當前類物件,由於無論建立多少個例項物件,但對於的類物件擁有隻有一個,所有在這樣的情況下物件鎖就是唯一的。下面我們看看如何使用將synchronized作用於靜態的increase方法。

synchronized作用於靜態方法

synchronized作用於靜態方法時,其鎖就是當前類的class物件鎖。由於靜態成員不專屬於任何一個例項物件,是類成員,因此通過class物件鎖可以控制靜態 成員的併發操作。需要注意的是如果一個執行緒A呼叫一個例項物件的非static synchronized方法,而執行緒B需要呼叫這個例項物件所屬類的靜態 synchronized方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法佔用的鎖是當前類的class物件,而訪問非靜態 synchronized 方法佔用的鎖是當前例項物件鎖,看如下程式碼

public class AccountingSyncClass implements Runnable{
    static int i=0;

    /**
     * 作用於靜態方法,鎖是當前class物件,也就是
     * AccountingSyncClass類對應的class物件
     */
    public static synchronized void increase(){
        i++;
    }

    /**
     * 非靜態,訪問時鎖不一樣不會發生互斥
     */
    public synchronized void increase4Obj(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新例項
        Thread t1=new Thread(new AccountingSyncClass());
        //new心事了
        Thread t2=new Thread(new AccountingSyncClass());
        //啟動執行緒
        t1.start();t2.start();

        t1.join();t2.join();
        System.out.println(i);
    }
}

由於synchronized關鍵字修飾的是靜態increase方法,與修飾例項方法不同的是,其鎖物件是當前類的class物件。注意程式碼中的increase4Obj方法是例項方法,其物件鎖是當前例項物件,如果別的執行緒呼叫該方法,將不會產生互斥現象,畢竟鎖物件不同,但我們應該意識到這種情況下可能會發現執行緒安全問題(操作了共享靜態變數i)。

synchronized同步程式碼塊

除了使用關鍵字修飾例項方法和靜態方法外,還可以使用同步程式碼塊,在某些情況下,我們編寫的方法體可能比較大,同時存在一些比較耗時的操作,而需要同步的程式碼又只有一小部分,如果直接對整個方法進行同步操作,可能會得不償失,此時我們可以使用同步程式碼塊的方式對需要同步的程式碼進行包裹,這樣就無需對整個方法進行同步操作了,同步程式碼塊的使用示例如下:

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗時操作....
        //使用同步程式碼塊對變數i進行同步操作,鎖物件為instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

從程式碼看出,將synchronized作用於一個給定的例項物件instance,即當前例項物件就是鎖物件,每次當執行緒進入synchronized包裹的程式碼塊時就會要求當前執行緒持有instance例項物件鎖,如果當前有其他執行緒正持有該物件鎖,那麼新到的執行緒就必須等待,這樣也就保證了每次只有一個執行緒執行i++;操作。當然除了instance作為物件外,我們還可以使用this物件(代表當前例項)或者當前類的class物件作為鎖,如下程式碼:

//this,當前例項物件鎖
synchronized(this){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

//class物件鎖
synchronized(AccountingSync.class){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

瞭解完synchronized的基本含義及其使用方式後,下面我們將進一步深入理解synchronized的底層實現原理。

synchronized底層語義原理

Java 虛擬機器中的同步(Synchronization)基於進入和退出管程(Monitor)物件實現, 無論是顯式同步(有明確的 monitorentermonitorexit 指令,即同步程式碼塊)還是隱式同步都是如此。在 Java 語言中,同步用的最多的地方可能是被 synchronized 修飾的同步方法。同步方法 並不是由 monitorentermonitorexit 指令來實現同步的,而是由方法呼叫指令讀取執行時常量池中方法的 ACC_SYNCHRONIZED 標誌來隱式實現的,關於這點,稍後詳細分析。下面先來了解一個概念Java物件頭,這對深入理解synchronized實現原理非常關鍵。

理解Java物件頭與Monitor

在JVM中,物件在記憶體中的佈局分為三塊區域:物件頭、例項資料和對齊填充。如下:
在這裡插入圖片描述

  • 例項變數:存放類的屬性資料資訊,包括父類的屬性資訊,如果是陣列的例項部分還包括陣列的長度,這部分記憶體按4位元組對齊。

  • 填充資料:由於虛擬機器要求物件起始地址必須是8位元組的整數倍。填充資料不是必須存在的,僅僅是為了位元組對齊,這點了解即可。

而對於頂部,則是Java頭物件,它實現synchronized的鎖物件的基礎,這點我們重點分析它,一般而言,synchronized使用的鎖物件是儲存在Java物件頭裡的,jvm中採用2個字來儲存物件頭(如果物件是陣列則會分配3個字,多出來的1個字記錄的是陣列長度),其主要結構是由Mark Word 和 Class Metadata Address 組成,其結構說明如下表:

虛擬機器位數 頭物件結構 說明
32/64bit Mark Word 儲存物件的hashCode、鎖資訊或分代年齡或GC標誌等資訊
32/64bit Class Metadata Address 型別指標指向物件的類元資料,JVM通過這個指標確定該物件是哪個類的例項。
其中Mark Word在預設情況下儲存著物件的HashCode、分代年齡、鎖標記位等以下是32位JVM的Mark Word預設儲存結構
鎖狀態 25bit 4bit 1bit是否是偏向鎖 2bit 鎖標誌位
無鎖狀態 物件HashCode 物件分代年齡 0 01
由於物件頭的資訊是與物件自身定義的資料沒有關係的額外儲存成本,因此考慮到JVM的空間效率,Mark Word 被設計成為一個非固定的資料結構,以便儲存更多有效的資料,它會根據物件本身的狀態複用自己的儲存空間,如32位JVM下,除了上述列出的Mark Word預設儲存結構外,還有如下可能變化的結構:

在這裡插入圖片描述

其中輕量級鎖和偏向鎖是Java 6 對 synchronized 鎖進行優化後新增加的,稍後我們會簡要分析。這裡我們主要分析一下重量級鎖也就是通常說synchronized的物件鎖,鎖標識位為10,其中指標指向的是monitor物件(也稱為管程或監視器鎖)的起始地址。每個物件都存在著一個 monitor 與之關聯,物件與其 monitor 之間的關係有存在多種實現方式,如monitor可以與物件一起建立銷燬或當執行緒試圖獲取物件鎖時自動生成,但當一個 monitor 被某個執行緒持有後,它便處於鎖定狀態。在Java虛擬機器(HotSpot)中,monitor是由ObjectMonitor實現的,其主要資料結構如下(位於HotSpot虛擬機器原始碼ObjectMonitor.hpp檔案,C++實現的)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的執行緒,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等待鎖block狀態的執行緒,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有兩個佇列,_WaitSet_EntryList,用來儲存ObjectWaiter物件列表( 每個等待鎖的執行緒都會被封裝成ObjectWaiter物件),_owner指向持有ObjectMonitor物件的執行緒,當多個執行緒同時訪問一段同步程式碼時,首先會進入 _EntryList 集合,當執行緒獲取到物件的monitor 後進入 _Owner 區域並把monitor中的owner變數設定為當前執行緒同時monitor中的計數器count1,若執行緒呼叫 wait() 方法,將釋放當前持有的monitorowner變數恢復為nullcount自減1,同時該執行緒進入 WaitSet集合中等待被喚醒。若當前執行緒執行完畢也將釋放monitor(鎖)並復位變數的值,以便其他執行緒進入獲取monitor(鎖)。如下圖所示
在這裡插入圖片描述
由此看來,monitor物件存在於每個Java物件的物件頭中(儲存的指標的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什麼Java中任意物件可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在於頂級物件Object中的原因(關於這點稍後還會進行分析),ok~,有了上述知識基礎後,下面我們將進一步分析synchronized在位元組碼層面的具體語義實現。

synchronized程式碼塊底層原理

public class SyncCodeBlock {

   public int i;

   public void syncTask(){
       //同步程式碼庫
       synchronized (this){
           i++;
       }
   }
}

編譯上述程式碼並使用javap反編譯後得到位元組碼如下(這裡我們省略一部分沒有必要的資訊):

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
  Last modified 2017-6-2; size 426 bytes
  MD5 checksum c80bc322c87b312de760942820b4fed5
  Compiled from "SyncCodeBlock.java"
public class com.zejian.concurrencys.SyncCodeBlock
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  //........省略常量池中資料
  //建構函式
  public com.zejian.concurrencys.SyncCodeBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
  //===========主要看看syncTask方法實現================
  public void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter  //注意此處,進入同步方法
         4: aload_0
         5: dup
         6: getfield      #2             // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2            // Field i:I
        14: aload_1
        15: monitorexit   //注意此處,退出同步方法
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit //注意此處,退出同步方法
        22: aload_2
        23: athrow
        24: return
      Exception table:
      //省略其他位元組碼.......
}
SourceFile: "SyncCodeBlock.java"

我們主要關注位元組碼中的如下程式碼

3: monitorenter  //進入同步方法
//..........省略其他  
15: monitorexit   //退出同步方法
16: goto          24
//省略其他.......
21: monitorexit //退出同步方法

從位元組碼中可知同步語句塊的實現使用的是monitorentermonitorexit 指令,其中monitorenter指令指向同步程式碼塊的開始位置,monitorexit指令則指明同步程式碼塊的結束位置,當執行monitorenter指令時,當前執行緒將試圖獲取 objectref(即物件鎖) 所對應的 monitor 的持有權,當 objectrefmonitor 的進入計數器為 0,那執行緒可以成功取得 monitor,並將計數器值設定為 1,取鎖成功。如果當前執行緒已經擁有 objectrefmonitor 的持有權,那它可以重入這個 monitor (關於重入性稍後會分析),重入時計數器的值也會加 1。倘若其他執行緒已經擁有 objectrefmonitor 的所有權,那當前執行緒將被阻塞,直到正在執行執行緒執行完畢,即monitorexit指令被執行,執行執行緒將釋放 monitor(鎖)並設定計數器值為0 ,其他執行緒將有機會持有 monitor 。值得注意的是編譯器將會確保無論方法通過何種方式完成,方法中呼叫過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而無論這個方法是正常結束還是異常結束。為了保證在方法異常完成時 monitorentermonitorexit 指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器宣告可處理所有的異常,它的目的就是用來執行 monitorexit 指令。從位元組碼中也可以看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。

synchronized方法底層原理

方法級的同步是隱式,即無需通過位元組碼指令來控制的,它實現在方法呼叫和返回操作之中。JVM可以從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標誌區分一個方法是否同步方法。當方法呼叫時,呼叫指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,如果設定了,執行執行緒將先持有monitor(虛擬機器規範中用的是"管程"一詞), 然後再執行方法,最後再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執行期間,執行執行緒持有了monitor,其他任何執行緒都無法再獲得同一個monitor。如果一個同步方法執行期間拋 出了異常,並且在方法內部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放。下面我們看看位元組碼層面如何實現:

public class SyncMethod {

   public int i;

   public synchronized void syncTask(){
           i++;
   }
}

使用javap反編譯後的位元組碼如下:

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
  Last modified 2017-6-2; size 308 bytes
  MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
  Compiled from "SyncMethod.java"
public class com.zejian.concurrencys.SyncMethod
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool;

   //省略沒必要的位元組碼
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法標識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}
SourceFile: "SyncMethod.java"

從位元組碼中可以看出,synchronized修飾的方法並沒有monitorenter指令和monitorexit指令,取得代之的確實是ACC_SYNCHRONIZED標識,該標識指明瞭該方法是一個同步方法,JVM通過該ACC_SYNCHRONIZED訪問標誌來辨別一個方法是否宣告為同步方法,從而執行相應的同步呼叫。這便是synchronized鎖在同步程式碼塊和同步方法上實現的基本原理。同時我們還必須注意到的是在Java早期版本中,synchronized屬於重量級鎖,效率低下,因為監視器鎖(monitor)是依賴於底層的作業系統的Mutex Lock來實現的,而作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的synchronized效率低的原因。慶幸的是在Java 6之後Java官方對從JVM層面對synchronized較大優化,所以現在的synchronized鎖效率也優化得很不錯了,Java 6之後,為了減少獲得鎖和釋放鎖所帶來的效能消耗,引入了輕量級鎖和偏向鎖,接下來我們將簡單瞭解一下Java官方在JVM層面對synchronized鎖的優化。

Java虛擬機器對synchronized的優化

鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級,關於重量級鎖,前面我們已詳細分析過,下面我們將介紹偏向鎖和輕量級鎖以及JVM的其他優化手段,這裡並不打算深入到每個鎖的實現和轉換過程更多地是闡述Java虛擬機器所提供的每個鎖的核心優化思想,畢竟涉及到具體過程比較繁瑣,如需瞭解詳細過程可以查閱《深入理解Java虛擬機器原理》。

偏向鎖

偏向鎖是Java 6之後加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,因此為了減少同一執行緒獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個執行緒獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個執行緒再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程式的效能。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個執行緒申請相同的鎖。但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的執行緒都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,並不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。下面我們接著瞭解輕量級鎖。

輕量級鎖

倘若偏向鎖失敗,虛擬機器並不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之後加入的),此時Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖能夠提升程式效能的依據是“對絕大部分的鎖,在整個同步週期內都不存在競爭”,注意這是經驗資料。需要了解的是,輕量級鎖所適應的場景是執行緒交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。

自旋鎖

輕量級鎖失敗後,虛擬機器為了避免執行緒真實地在作業系統層面掛起,還會進行一項稱為自旋鎖的優化手段。這是基於在大多數情況下,執行緒持有鎖的時間都不會太長,如果直接掛起作業系統層面的執行緒可能會得不償失,畢竟作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的執行緒可以獲得鎖,因此虛擬機器會讓當前想要獲取鎖的執行緒做幾個空迴圈(這也是稱為自旋的原因),一般不會太久,可能是50個迴圈或100迴圈,在經過若干次迴圈後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將執行緒在作業系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最後沒辦法也就只能升級為重量級鎖了。

鎖消除

消除鎖是虛擬機器另外一種鎖的優化,這種優化更徹底,Java虛擬機器在JIT編譯時(可以簡單理解為當某段程式碼即將第一次被執行時進行編譯,又稱即時編譯),通過對執行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬於一個區域性變數,並且不會被其他執行緒所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。

/**
 * Created by zejian on 2017/6/4.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 * 消除StringBuffer同步鎖
 */
public class StringBufferRemoveSync {

    public void add(String str1, String str2) {
        //StringBuffer是執行緒安全,由於sb只會在append方法中使用,不可能被其他執行緒引用
        //因此sb屬於不可能共享的資源,JVM會自動消除內部的鎖
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
        for (int i = 0; i < 10000000; i++) {
            rmsync.add("abc", "123");
        }
    }

}

關於synchronized 可能需要了解的關鍵點

synchronized的可重入性

從互斥鎖的設計上來說,當一個執行緒試圖操作一個由其他執行緒持有的物件鎖的臨界資源時,將會處於阻塞狀態,但當一個執行緒再次請求自己持有物件鎖的臨界資源時,這種情況屬於重入鎖,請求將會成功,在java中synchronized是基於原子性的內部鎖機制,是可重入的,因此在一個執行緒呼叫synchronized方法的同時在其方法體內部呼叫該物件另一個synchronized方法,也就是說一個執行緒得到一個物件鎖後再次請求該物件鎖,是允許的,這就是synchronized的可重入性。如下:

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){

            //this,當前例項物件鎖
            synchronized(this){
                i++;
                increase();//synchronized的可重入性
            }
        }
    }

    public synchronized void increase(){
        j++;
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

正如程式碼所演示的,在獲取當前例項物件鎖後進入synchronized程式碼塊執行同步程式碼,並在程式碼塊中呼叫了當前例項物件的另外一個synchronized方法,再次請求當前例項鎖時,將被允許,進而執行方法體程式碼,這就是重入鎖最直接的體現,需要特別注意另外一種情況,當子類繼承父類時,子類也是可以通過可重入鎖呼叫父類的同步方法。注意由於synchronized是基於monitor實現的,因此每次重入,monitor中的計數器仍會加1。

執行緒中斷與synchronized

執行緒中斷

正如中斷二字所表達的意義,線上程執行(run方法)中間打斷它,在Java中,提供了以下3個有關執行緒中斷的方法

//中斷執行緒(例項方法)
public void Thread.interrupt();

//判斷執行緒是否被中斷(例項方法)
public boolean Thread.isInterrupted();

//判斷是否被中斷並清除當前中斷狀態(靜態方法)
public static boolean Thread.interrupted();

當一個執行緒處於被阻塞狀態或者試圖執行一個阻塞操作時,使用Thread.interrupt()方式中斷該執行緒,注意此時將會丟擲一個InterruptedException的異常,同時中斷狀態將會被複位(由中斷狀態改為非中斷狀態),如下程式碼將演示該過程:

public class InterruputSleepThread3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                //while在try中,通過異常中斷就可以退出run迴圈
                try {
                    while (true) {
                        //當前執行緒處於阻塞狀態,異常必須捕捉處理,無法往外丟擲
                        TimeUnit.SECONDS.sleep(2);
                    }
                } catch (InterruptedException e) {
                    System.out.println("Interruted When Sleep");
                    boolean interrupt = this.isInterrupted();
                    //中斷狀態被複位
                    System.out.println("interrupt:"+interrupt);
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        //中斷處於阻塞狀態的執行緒
        t1.interrupt();

        /**
         * 輸出結果:
           Interruted When Sleep
           interrupt:false
         */
    }
}

如上述程式碼所示,我們建立一個執行緒,並在執行緒中呼叫了sleep方法從而使用執行緒進入阻塞狀態,啟動執行緒後,呼叫執行緒例項物件的interrupt方法中斷阻塞異常,並丟擲InterruptedException異常,此時中斷狀態也將被複位。這裡有些人可能會詫異,為什麼不用Thread.sleep(2000);而是用TimeUnit.SECONDS.sleep(2);其實原因很簡單,前者使用時並沒有明確的單位說明,而後者非常明確表達秒的單位,事實上後者的內部實現最終還是呼叫了Thread.sleep(2000);,但為了編寫的程式碼語義更清晰,建議使用TimeUnit.SECONDS.sleep(2);的方式,注意TimeUnit是個列舉型別。ok~,除了阻塞中斷的情景,我們還可能會遇到處於執行期且非阻塞的狀態的執行緒,這種情況下,直接呼叫Thread.interrupt()中斷執行緒是不會得到任響應的,如下程式碼,將無法中斷非阻塞狀態下的執行緒:

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    System.out.println("未被中斷");
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /**
         * 輸出結果(無限執行):
             未被中斷
             未被中斷
             未被中斷
             ......
         */
    }
}

雖然我們呼叫了interrupt方法,但執行緒t1並未被中斷,因為處於非阻塞狀態的執行緒需要我們手動進行中斷檢測並結束程式,改進後代碼如下:

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    //判斷當前執行緒是否被中斷
                    if (this.isInterrupted()){
                        System.out.println("執行緒中斷");
                        break;
                    }
                }

                System.out.println("已跳出迴圈,執行緒中斷!");
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /**
         * 輸出結果:
            執行緒中斷
            已跳出迴圈,執行緒中斷!
         */
    }
}

是的,我們在程式碼中使用了例項方法isInterrupted判斷執行緒是否已被中斷,如果被中斷將跳出迴圈以此結束執行緒,注意非阻塞狀態呼叫interrupt()並不會導致中斷狀態重置。綜合所述,可以簡單總結一下中斷兩種情況,一種是當執行緒處於阻塞狀態或者試圖執行一個阻塞操作時,我們可以使用例項方法interrupt()進行執行緒中斷,執行中斷操作後將會丟擲interruptException異常(該異常必須捕捉無法向外丟擲)並將中斷狀態復位,另外一種是當執行緒處於執行狀態時,我們也可呼叫例項方法interrupt()進行執行緒中斷,但同時必須手動判斷中斷狀態,並編寫中斷執行緒的程式碼(其實就是結束run方法體的程式碼)。有時我們在編碼時可能需要兼顧以上兩種情況,那麼就可以如下編寫:

public void run(){
    try {
    //判斷當前執行緒是否已中斷,注意interrupted方法是靜態的,執行後會對中斷狀態進行復位
    while (!Thread.interrupted()) {
        TimeUnit.SECONDS.sleep(2);
    }
    } catch (InterruptedException e) {

    }
}

中斷與synchronized

事實上執行緒的中斷操作對於正在等待獲取的鎖物件的synchronized方法或者程式碼塊並不起作用,也就是對於synchronized來說,如果一個執行緒在等待鎖,那麼結果只有兩種,要麼它獲得這把鎖繼續執行,要麼它就儲存等待,即使呼叫中斷執行緒的方法,也不會生效。演示程式碼如下

/**
 * Created by zejian on 2017/6/2.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
public class SynchronizedBlocked implements Runnable{

    public synchronized void f() {
        System.out.println("Trying to call f()");
        while(true) // Never releases lock
            Thread.yield();
    }

    /**
     * 在構造器中建立新執行緒並啟動獲取物件鎖
     */
    public SynchronizedBlocked() {
        //該執行緒已持有當前例項鎖
        new Thread() {
            public void run() {
                f(); // Lock acquired by this thread
            }
        }.start();
    }
    public void run() {
        //中斷判斷
        while (true) {
            if (Thread.interrupted()) {
                System.out.println("中斷執行緒!!");
                break;
            } else {
                f();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        SynchronizedBlocked sync = new SynchronizedBlocked();
        Thread t = new Thread(sync);
        //啟動後呼叫f()方法,無法獲取當前例項鎖處於等待狀態
        t.start();
        TimeUnit.SECONDS.sleep(1);
        //中斷執行緒,無法生效
        t.interrupt();
    }
}

我們在SynchronizedBlocked建構函式中建立一個新執行緒並啟動獲取呼叫f()獲取到當前例項鎖,由於SynchronizedBlocked自身也是執行緒,啟動後在其run方法中也呼叫了f(),但由於物件鎖被其他執行緒佔用,導致t執行緒只能等到鎖,此時我們呼叫了t.interrupt();但並不能中斷執行緒。

等待喚醒機制與synchronized

所謂等待喚醒機制本篇主要指的是notify/notifyAllwait方法,在使用這3個方法時,必須處於synchronized程式碼塊或者synchronized方法中,否則就會丟擲IllegalMonitorStateException異常,這是因為呼叫這幾個方法前必須拿到當前物件的監視器monitor物件,也就是說notify/notifyAllwait方法依賴於monitor物件,在前面的分析中,我們知道monitor 存在於物件頭的Mark Word 中(儲存monitor引用指標),而synchronized關鍵字可以獲取 monitor ,這也就是為什麼notify/notifyAllwait方法必須在synchronized程式碼塊或者synchronized方法呼叫的原因。

synchronized (obj) {
       obj.wait();
       obj.notify();
       obj.notifyAll();         
 }

需要特別理解的一點是,與sleep方法不同的是wait方法呼叫完成後,執行緒將被暫停,但wait方法將會釋放當前持有的監視器鎖(monitor),直到有執行緒呼叫notify/notifyAll方法後方能繼續執行,而sleep方法只讓執行緒休眠並不釋放鎖。同時notify/notifyAll方法呼叫後,並不會馬上釋放監視器鎖,而是在相應的synchronized(){}/synchronized方法執行結束後才自動釋放鎖。

相關推薦

原始碼閱讀Java併發synchronized實現原理

執行緒安全是併發程式設計中的重要關注點,應該注意到的是,造成執行緒安全問題的主要誘因有兩點,一是存在共享資料(也稱臨界資源),二是存在多條執行緒共同操作共享資料。因此為了解決這個問題,我們可能需要這樣一個方案,當存在多個執行緒操作共享資料時,需要保證同一時刻有且

深入理解Java併發synchronized實現原理

關聯文章: 本篇主要是對Java併發中synchronized關鍵字進行較為深入的探索,這些知識點結合博主對synchronized的個人理解以及相關的書籍的講解(在結尾參考資料),如有誤處,歡迎留言。 執行緒安全是併發程式

深入理解 Java 併發 synchronized 實現原理

關聯文章深入理解Java型別資訊(Class物件)與反射機制深入理解Java列舉型別(enum)深入理解Java註解型別(@Annotation)深入理解Java併發之synchronized實現原理本篇主要是對Java併發中synchronized關鍵字進行較為深入的探索,這些知識點結合博主對synchro

java 併發 synchronized 實現原理

在 java 開發中 synchronized 是使用的最多的工具。 表現形式 在 java 中每個物件都可以作為鎖: 對於普通同步方法,鎖是當前例項物件; 對於靜態同步方法,鎖是當前類的 Class 物件; 對於同步方法快,鎖是 Synchronized 括

原始碼閱讀基於併發AQS的(獨佔鎖)重入鎖(ReetrantLock)及其Condition實現原理

Lock介面 前面我們詳談過解決多執行緒同步問題的關鍵字synchronized,synchronized屬於隱式鎖,即鎖的持有與釋放都是隱式的,我們無需干預,而本篇我們要講解的是顯式鎖,即鎖的持有和釋放都必須由我們手動編寫。在Java 1.5中,官方在conc

Java程式設計技術分享Java併發Fork-Join框架分析

1、什麼是Fork/Join框架 及產生背景 Fork/Join框架是Java7提供了的一個用於並行執行任務的框架, 是一個把大任務分割成若干個小任務,最終彙總每個小任務結果後得到大任務結果的框架。上邊是書上的定義。 我們用粗話說:Fork/Join是一個框架,來解決執行效率,手段是並行,但

java併發----synchronized與ReenTrantLock

Java 提供了兩種鎖機制來控制多個執行緒對共享資源的互斥訪問,第一個是 JVM 實現的 synchronized,而另一個是 JDK 實現的 ReentrantLock。 synchronized synchronized關鍵字最主要幾種使用方式: (1)同步一個程式碼塊: 只作用

原始碼閱讀JAVA中的CAS詳解

     本篇的思路是先闡明無鎖執行者CAS的核心演算法原理然後分析Java執行CAS的實踐者Unsafe類,該類中的方法都是native修飾的,因此我們會以說明方法作用為主介紹Unsafe類,最後再介紹併發包中的Atomic系統使用CAS原理實現的併發類。

Java併發機制底層實現原理synchronized

章節目錄 synchronized的實現原理與應用 synchronized 重量級鎖 1.6版本之前 synchronized 被稱之為 重量級鎖 1.6版本對 synchronized 進行了優化,主要優化的點在於 減少 獲得鎖和釋放鎖帶 來的效能消耗,為實現這個目的引入了偏向鎖

Java併發synchronized關鍵字和Lock介面

歡迎點贊閱讀,一同學習交流,有疑問請留言 。 GitHub上也有開源 JavaHouse,歡迎star 引用 當開發過程中,我們遇到併發問題。怎麼解決? 一種解決方式,簡單粗暴:上鎖。將千軍萬馬都給攔下來,只允許一個人過獨木橋。書面意思就是將並行的程式變成序列的程式。現實的鎖有門鎖、掛鎖和抽屜鎖等等。

Java併發synchronized關鍵字深度解析(一)

前言         近期研讀路神之絕世武學,徜徉於浩瀚無垠知識之海洋,偶有攫取吉光片羽,惶恐未領略其精髓即隱入歲月深處,遂急忙記錄一二,順備來日吹cow之談資。本小系列為併發之親兒子-獨臂狂俠synchronized專場。 一、使用場景     &

《提升能力,漲薪可待》—Java併發Synchronized

Synchronized簡介 執行緒安全是併發程式設計中的至關重要的,造成執行緒安全問題的主要原因: 臨界資源, 存在共享資料 多執行緒共同操作共享資料 而Java關鍵字synchronized,為多執行緒場景下防止臨界資源訪問衝突提供支援, 可以保證在同一時刻,只有一個執行緒可以執行某個方

java併發synchronized

Java為我們提供了隱式(synchronized宣告方式)和顯式(java.util.concurrentAPI程式設計方式)兩種工具來避免執行緒爭用。 本章節探索Java關鍵字synchronized。主要包含以下幾個內容。 - synchronized關鍵字的使用; - synchronized背後

java併發 CopyOnWriteArrayList的原理和使用方法

描述 CopyOnWriteArrayList:CopyOnWriteArrayList這是一個ArrayList的執行緒安全的變體,其原理大概可以通俗的理解為:初始化的時候只有一個容器,很常一段時間,這個容器資料、數量等沒有發生變化的時候,大家(多個執行緒),都是讀取(假設這段時間裡只

Java併發(三)synchronized實現原理

一、synchronized用法 Java中的同步塊用synchronized標記。 同步塊在Java中是同步在某個物件上(監視器物件)。 所有同步在一個物件上的同步塊在同時只能被一個執行緒進入並執行操作。 所有其他等待進入該同步塊的執行緒將被阻塞,直到執行該同步塊中的執行緒退出。 (注:不要使用全

併發程式設計(八)—— Java 併發佇列 BlockingQueue 實現 ArrayBlockingQueue 原始碼分析

開篇先介紹下 BlockingQueue 這個介面的規則,後面再看其實現。 阻塞佇列概要 阻塞佇列與我們平常接觸的普通佇列(LinkedList或ArrayList等)的最大不同點,在於阻塞佇列的阻塞新增和阻塞刪除方法。 阻塞新增 所謂的阻塞新增是指當阻塞佇列元素已滿時,佇列會阻塞加入元素的執行緒,直佇

併發程式設計(十二)—— Java 執行緒池 實現原理原始碼深度解析 submit方法 (二)

在上一篇《併發程式設計(十一)—— Java 執行緒池 實現原理與原始碼深度解析(一)》中提到了執行緒池ThreadPoolExecutor的原理以及它的execute方法。這篇文章是接著上一篇文章寫的,如果你沒有閱讀上一篇文章,建議你去讀讀。本文解析ThreadPoolExecutor#submit。  

Java併發深入分析synchronized實現原理

稍微進行了整合目前在Java中存在兩種鎖機制:synchronized和Lock,Lock介面及其實現類是JDK5增加的內容,synchronized是個重量級鎖,是解決併發問題的一種最常用的方法,也是最簡單的一種方法,Java SE1.6對其進行了優化。synchroniz

Java併發原始碼學習系列阻塞佇列BlockingQueue及實現原理分析

[toc] 系列傳送門: - [Java併發包原始碼學習系列:AbstractQueuedSynchronizer](https://blog.csdn.net/Sky_QiaoBa_Sum/article/details/112254373) - [Java併發包原始碼學習系列:CLH同步佇列及同步資源

Java併發Condition的實現分析

一、Condition的概念介紹回憶 synchronized 關鍵字,它配合 Object 的 wait()、notify() 系列方法可以實現等待/通知模式。 對於 Lock,通過 Condition 也可以實現等待/通知模式。 Condition 是一個介面。 Condition 介面的實現類是