synchronized實現原理

0. 前言

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

1. synchronized的三種應用方式

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

  • 修飾實例方法,做用於當前實例加鎖,進入同步代碼前要得到當前實例的鎖
  • 修飾靜態方法,做用於當前類對象加鎖,進入同步代碼前要得到當前類對象的鎖
  • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼塊前要得到給定對象的鎖

1.1 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<100;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();
        System.out.println(i);//200
    }
    
}

複製代碼

上述代碼中,咱們開啓兩個線程操做同一個共享資源即變量i,因爲i++操做並不具有原子性,該操做是先讀取值,而後寫回一個新值,至關於原來的值加上1,分兩步完成。若是第二個線程在第一個線程讀取舊值和寫回新值期間讀取i的值,那麼第二個線程就會與第一個線程一塊兒看到同一個值,並執行相同值的加1操做,這也就形成了線程安全失敗。所以對於increase方法必須使用synchronized修飾,以便保證線程安全。此時咱們應該注意到synchronized修飾的是實例方法increase,在這樣的狀況下,當前線程的鎖即是實例對象,注意Java中的線程同步鎖能夠是任意對象。從代碼執行結果來看確實是正確的,假若咱們沒有使用synchronized關鍵字,其最終輸出結果就極可能小於200,這即是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();
        System.out.println(i);//146
    }
}
複製代碼

上述代碼與前面不一樣的是咱們同時建立了兩個新實例AccountingSyncBad,而後啓動兩個不一樣的線程對共享變量i進行操做,但很遺憾操做結果是146而不是指望結果200,由於上述代碼犯了嚴重的錯誤,雖然咱們使用synchronized修飾了increase方法,但卻new了兩個不一樣的實例對象,這也就意味着存在着兩個不一樣的實例對象鎖,所以t1和t2都會進入各自的對象鎖,也就是說t1和t2線程使用的是不一樣的鎖,所以線程安全是沒法保證的。解決這種困境的的方式是將synchronized做用於靜態的increase方法,這樣的話,對象鎖就當前類對象,因爲不管建立多少個實例對象,但對於的類對象只有一個,全部在這樣的狀況下對象鎖就是惟一的。下面咱們看看如何使用將synchronized做用於靜態的increase方法。數據結構

1.2 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();
        System.out.println(i);
    }
}

複製代碼

因爲synchronized關鍵字修飾的是靜態increase方法,與修飾實例方法不一樣的是,其鎖對象是當前類的class對象。注意代碼中的increase4Obj方法是實例方法,其對象鎖是當前實例對象,若是別的線程調用該方法,將不會產生互斥現象,畢竟鎖對象不一樣,但咱們應該意識到這種狀況下可能會發現線程安全問題(操做了共享靜態變量i)。併發

1.3 synchronized同步代碼塊

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

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();
        System.out.println(i);
    }
}
複製代碼

從代碼看出,將synchronized做用於一個給定的實例對象instance,即當前實例對象就是鎖對象,每次當線程進入synchronized包裹的代碼塊時就會要求當前線程持有instance實例對象鎖,若是當前有其餘線程正持有該對象鎖,那麼新到的線程就必須等待,這樣也就保證了每次只有一個線程執行i++;操做。固然除了instance做爲對象外,咱們還可使用this對象(表明當前實例)或者當前類的class對象做爲鎖,以下代碼:ide

//this,當前實例對象鎖
synchronized(this){
    for(int j=0;j<100;j++){
        i++;
    }
}

//class對象鎖
synchronized(AccountingSync.class){
    for(int j=0;j<100;j++){
        i++;
    }
}
複製代碼

3. synchronized底層語義原理

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

3.1 理解Java對象頭與Monitor

在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例數據和對齊填充。以下:

對象存儲佈局

  • 實例數據:存放類的屬性數據信息,包括父類的屬性信息。
  • 對齊填充:因爲虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊。
  • 對象頭:Java對象頭通常佔有2個機器碼(在32位虛擬機中,1個機器碼等於4字節,也就是32bit,在64位虛擬機中,1個機器碼是8個字節,也就是64bit),可是若是對象是數組類型,則須要3個機器碼,由於JVM虛擬機能夠經過Java對象的元數據信息肯定Java對象的大小,可是沒法從數組的元數據來確認數組的大小,因此用一塊來記錄數組長度。

爲了表示對象的屬性、方法等信息,不得不須要結構描述。Hotspot VM 使用對象頭部的一個指針指向 Class 區域的方式來找到對象的 Class 描述,以及內部的方法、屬性入口。以下圖所示:

虛擬機位數 頭對象結構 說明
32/64bit Mark Word 存儲對象自身的運行時數據,如哈希碼(HashCode)、GC 分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳、對象分代年齡等;Mark Word 被設計成一個非固定的數據結構以便在極小的空間內存儲儘可能多的信息,它會根據本身的狀態複用本身的存儲空間
32/64bi Class Metadata Address 類型指針指向對象的類元數據,JVM經過這個指針肯定該對象是哪一個類的實例。
32/64bi Array Length 若是對象是一個 Java 數組,那在對象頭中還必須有一塊用於記錄數組長度的數據。由於虛擬機能夠經過普通 Java 對象的元數據信息肯定 Java 對象的大小,可是從數組的元數據中沒法肯定數組的大小。這部分數據的長度在 32 位和 64 位的虛擬機(未開啓壓縮指針)中分別爲 32bit 和 64bit。

例如,在 32 位的 HotSpot 虛擬機中,若是對象處於未被鎖定的狀態下,那麼 Mark Word 的 32bit 空間中的 25bit 用於存儲對象哈希碼,4bit 用於存儲對象分代年齡,2bit 用於存儲鎖標誌位,1bit 固定爲 0,以下表所示:

無鎖狀態
因爲對象頭的信息是與對象自身定義的數據沒有關係的額外存儲成本,所以考慮到JVM的空間效率,Mark Word 被設計成爲一個非固定的數據結構,以便存儲更多有效的數據,它會根據對象自己的狀態複用本身的存儲空間,如32位JVM下,除了上述列出的Mark Word默認存儲結構外,還有以下可能變化的結構:

在64位虛擬機下,Mark Word是64bit大小的,其存儲結構以下:

對象頭的最後兩位存儲了鎖的標誌位,01是初始狀態,未加鎖,其對象頭裏存儲的是對象自己的哈希碼,隨着鎖級別的不一樣,對象頭裏會存儲不一樣的內容。偏向鎖存儲的是當前佔用此對象的線程ID;而輕量級則存儲指向線程棧中鎖記錄的指針。從這裏咱們能夠看到,「鎖」這個東西,多是個鎖記錄+對象頭裏的引用指針(判斷線程是否擁有鎖時將線程的鎖記錄地址和對象頭裏的指針地址比較),也多是對象頭裏的線程ID(判斷線程是否擁有鎖時將線程的ID和對象頭裏存儲的線程ID比較)。

其中輕量級鎖和偏向鎖是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中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其餘線程進入獲取monitor(鎖)。以下圖所示

由此看來,monitor對象存在於每一個Java對象的對象頭中(存儲的指針的指向),synchronized鎖即是經過這種方式獲取鎖的,也是爲何Java中任意對象能夠做爲鎖的緣由,同時也是notify/notifyAll/wait等方法存在於頂級對象Object中的緣由(關於這點稍後還會進行分析),有了上述知識基礎後,下面咱們將進一步分析synchronized在字節碼層面的具體語義實現。

3.2 synchronized代碼塊底層原理

如今咱們從新定義一個synchronized修飾的同步代碼塊,在代碼塊中操做共享變量i,以下

public class SyncCodeBlock {

   public int i;

   public void syncTask(){
       //同步代碼塊
       synchronized (this){
           i++;
       }
   }
}
複製代碼

編譯上述代碼並使用javap反編譯後獲得字節碼以下(這裏咱們省略一部分沒有必要的信息):

Classfile /***/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
  Last modified 2018-07-25; size 426 bytes
  MD5 checksum c80bc322c87b312de760942820b4fed5
  Compiled from "SyncCodeBlock.java"
public class com.hc.concurrencys.SyncCodeBlock
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  //........省略常量池中數據
  //構造函數
  public com.hc.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 //退出同步方法
複製代碼

從字節碼中可知同步語句塊的實現使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置,當執行monitorenter指令時,當前線程將試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器爲 0,那線程能夠成功取得 monitor,並將計數器值設置爲 1,取鎖成功。若是當前線程已經擁有 objectref 的 monitor 的持有權,那它能夠重入這個 monitor (關於重入性稍後會分析),重入時計數器的值也會加 1。假若其餘線程已經擁有 objectref 的 monitor 的全部權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值爲0 ,其餘線程將有機會持有 monitor 。值得注意的是編譯器將會確保不管方法經過何種方式完成,方法中調用過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而不管這個方法是正常結束仍是異常結束。爲了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然能夠正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理全部的異常,它的目的就是用來執行 monitorexit 指令。從字節碼中也能夠看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。

3.3 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 /***/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.hc.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效率低的緣由。慶幸的是在Java6以後Java官方對從JVM層面對synchronized較大優化,因此如今的synchronized鎖效率也優化得很不錯了,Java6以後,爲了減小得到鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖,接下來咱們將簡單瞭解一下Java官方在JVM層面對synchronized鎖的優化。

4. Java虛擬機對synchronized的優化

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

4.1 偏向鎖

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

4.2 輕量級鎖

假若偏向鎖失敗,虛擬機並不會當即升級爲重量級鎖,它還會嘗試使用一種稱爲輕量級鎖的優化手段(1.6以後加入的),此時Mark Word 的結構也變爲輕量級鎖的結構。輕量級鎖可以提高程序性能的依據是「對絕大部分的鎖,在整個同步週期內都不存在競爭」,注意這是經驗數據。須要瞭解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,若是存在同一時間訪問同一鎖的場合,就會致使輕量級鎖膨脹爲重量級鎖。

4.3 自旋鎖

輕量級鎖失敗後,虛擬機爲了不線程真實地在操做系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。這是基於在大多數狀況下,線程持有鎖的時間都不會太長,若是直接掛起操做系統層面的線程可能會得不償失,畢竟操做系統實現線程之間的切換時須要從用戶態轉換到核心態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高,所以自旋鎖會假設在不久未來,當前的線程能夠得到鎖,所以虛擬機會讓當前想要獲取鎖的線程作幾個空循環(這也是稱爲自旋的緣由),通常不會過久,多是50個循環或100循環,在通過若干次循環後,若是獲得鎖,就順利進入臨界區。若是還不能得到鎖,那就會將線程在操做系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是能夠提高效率的。最後沒辦法也就只能升級爲重量級鎖了。

4.4 適應性自旋鎖

DK 1.6引入了更加聰明的自旋鎖,即自適應自旋鎖。所謂自適應就意味着自旋的次數再也不是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。

線程若是自旋成功了,那麼下次自旋的次數會更加多,由於虛擬機認爲既然上次成功了,那麼這次自旋也頗有可能會再次成功,那麼它就會容許自旋等待持續的次數更多。反之,若是對於某個鎖,不多有自旋可以成功,那麼在之後要或者這個鎖的時候自旋的次數會減小甚至省略掉自旋過程,以避免浪費處理器資源。

4.5 鎖消除

消除鎖是虛擬機另一種鎖的優化,這種優化更完全,Java虛擬機在JIT編譯時(能夠簡單理解爲當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),經過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,經過這種方式消除沒有必要的鎖,能夠節省毫無心義的請求鎖時間,以下StringBuffer的append是一個同步方法,可是在add方法中的StringBuffer屬於一個局部變量,而且不會被其餘線程所使用,所以StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。

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 < 100; i++) {
            rmsync.add("abc", "123");
        }
    }

}
複製代碼

4.6 偏向鎖、輕量級鎖、重量級鎖之間的狀態轉換

5. 關於synchronized 可能須要瞭解的關鍵點

5.1 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<100;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();
        t1.join();
        t2.start();
        t2.join();
        System.out.println(i);
    }
}
複製代碼

正如代碼所演示的,在獲取當前實例對象鎖後進入synchronized代碼塊執行同步代碼,並在代碼塊中調用了當前實例對象的另一個synchronized方法,再次請求當前實例鎖時,將被容許,進而執行方法體代碼,這就是重入鎖最直接的體現,須要特別注意另一種狀況,當子類繼承父類時,子類也是能夠經過可重入鎖調用父類的同步方法。注意因爲synchronized是基於monitor實現的,所以每次重入,monitor中的計數器仍會加1

重入進一步提高了加鎖行爲的封裝性,所以簡化了面向對象併發代碼的開發。分析以下程序:

public class Father {  
        public synchronized void doSomething(){  
            // do something... 
        }  
    }  
      
    public class Child extends Father {  
        public synchronized void doSomething(){  
            // do something... 
            super.doSomething();  
        }  
    }  
複製代碼

子類覆寫了父類的同步方法,而後調用父類中的方法,此時若是沒有可重入的鎖,那麼這段代碼將產生死鎖。

因爲Father和Child中的doSomething方法都是synchronized方法,所以每一個doSomething方法在執行前都會獲取Child對象實例上的鎖。若是內置鎖不是可重入的,那麼在調用super.doSomething時將沒法得到該Child對象上的互斥鎖,由於這個鎖已經被持有,從而線程會永遠阻塞下去,一直在等待一個永遠也沒法獲取的鎖。重入則避免了這種死鎖狀況的發生。

同一個線程在調用本類中其餘synchronized方法、塊或父類中的synchronized方法/塊時,都不會阻礙該線程的執行,由於互斥鎖是可重入的。

5.2 線程中斷與synchronized

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

//中斷線程(實例方法)
public void Thread.interrupt();

//判斷線程是否被中斷(實例方法)
public boolean Thread.isInterrupted();

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

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

public class InterruputSleepThread {
    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是個枚舉類型。除了阻塞中斷的情景,咱們還可能會遇處處於運行期且非阻塞的狀態的線程,這種狀況下,直接調用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來講,若是一個線程在等待鎖,那麼結果只有兩種,要麼它得到這把鎖繼續執行,要麼它就保存等待,即便調用中斷線程的方法,也不會生效。演示代碼以下:

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();但並不能中斷線程。

5.3 等待喚醒機制與synchronized

所謂等待喚醒機制本篇主要指的是notify/notifyAll和wait方法,在使用這3個方法時,必須處於synchronized代碼塊或者synchronized方法中,不然就會拋出IllegalMonitorStateException異常,這是由於調用這幾個方法前必須拿到當前對象的監視器monitor對象,也就是說notify/notifyAll和wait方法依賴於monitor對象,在前面的分析中,咱們知道monitor 存在於對象頭的Mark Word 中(存儲monitor引用指針),而synchronized關鍵字能夠獲取 monitor ,這也就是爲何notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調用的緣由。

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

相關文章
相關標籤/搜索