淺談Synchronized

Synchronized

Java在互斥同步方面上除了提供Lock API外,還提供Synchronized關鍵字來實現互斥同步原語。Synchronized是jdk內建的鎖同步機制,在jdk1.6版本以前,Synchronized是java實現互斥同步的惟一方式。java

Synchronized的使用有三種方式,做爲一個關鍵字,可做用於代碼塊、普通方法和靜態方法,做用於代碼塊表示對當前代碼塊加鎖,若是其餘線程同時訪問同一個對象該代碼塊,將會被阻塞;若是做用於普通方法,那麼當有多個線程同時訪問同一個對象的這個方法時,只有一個線程能執行該方法,其餘線程將等待先前的線程釋放才能夠執行;若是做用於靜態方法,那麼若是多個線程訪問該類的這個靜態方法時,其餘線程將等待先前的線程釋放才能夠執行。
接下來咱們經過一個例子來看它的基本用法。數據結構

/**
 * v
 * 2020/3/17 9:23 下午
 * 1.0
 */
public class SynchronizedTest {
    
    public void method1() {
        // 做用於代碼塊
        synchronized (this) {
            System.out.println("this is method1");
        }
    }

    // 做用於普通方法
    public synchronized void method2() {
        System.out.println("this is method2");
        
    }

    // 做用於靜態方法
    public static synchronized void method3() {
        System.out.println("this is method3");
    }
    

    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();
        test.method1();
        test.method2();
        SynchronizedTest.method3();
    }
}

因爲synchronized是java內建的關鍵字,由於synchronized的實現原理並不能從java語言層面去分析,只能經過實現java的C語言中去分析,這裏限於我的能力也沒辦法拿出來分享一下。不過咱們能夠從它的字節碼來看一下,被synchronized修飾的代碼塊有什麼不一樣。jvm

這裏咱們看一下上面代碼編譯出來的字節碼,主要看一下method一、method2和method3這三個方法的字節碼。佈局

// class version 52.0 (52)
// access flags 0x21
public class cn/v/SynchronizedTest {

  // compiled from: SynchronizedTest.java

  ...省略
  // access flags 0x1
  public method1()V
    TRYCATCHBLOCK L0 L1 L2 null
    TRYCATCHBLOCK L2 L3 L2 null
   L4
    LINENUMBER 14 L4
    ALOAD 0
    DUP
    ASTORE 1
    MONITORENTER
   L0
    LINENUMBER 15 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "this is method1"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L5
    LINENUMBER 16 L5
    ALOAD 1
    MONITOREXIT
   L1
    GOTO L6
   L2
   FRAME FULL [cn/v/SynchronizedTest java/lang/Object] [java/lang/Throwable]
    ASTORE 2
    ALOAD 1
    MONITOREXIT
   L3
    ALOAD 2
    ATHROW
   L6
    LINENUMBER 17 L6
   FRAME CHOP 1
    RETURN
   L7
    LOCALVARIABLE this Lcn/v/SynchronizedTest; L4 L7 0
    MAXSTACK = 2
    MAXLOCALS = 3

  // access flags 0x21
  public synchronized method2()V
   L0
    LINENUMBER 21 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "this is method2"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 23 L1
    RETURN
   L2
    LOCALVARIABLE this Lcn/v/SynchronizedTest; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x29
  public static synchronized method3()V
   L0
    LINENUMBER 27 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "this is method3"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 28 L1
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0

    ...省略
}

咱們能夠看出,在method1中,被synchronized修飾的同步代碼塊的入口和出口,分別插入了MONITORENTOR、MONITOREXIT字節碼指令。然而,在Mthod2和method3中,並無任何特別的字節碼指令對它們進行修飾,這是由於在Class文件的方法表中,將該方法的access_flag字段中的synchronized設置爲1,表示該方法是同步方法並使用調用該方法的對象或該方法所屬的Class在JVM的內部對象表示Class做爲鎖對象。性能

在jvm中,每一個對象都有一個monitor監控器,MONITORENTER主要就是嘗試獲取這個monitor監視器,若是成功獲取monitor,就將值+1,MONITOREXIT就是在線程離開同步代碼塊時,對其值-1。若是線程重入,就將值再-1,synchronized是能夠重入的。ReentranLock的實現原理相似,也是內部維護一個volitile int類型的變量,經過cas操做對其加一減一來表示鎖的獲取和釋放。學習

咱們上邊說過,synchronized在修飾方法的時候,是經過在其Class文件的方法表中,將該方法的access_flag字段中的synchronized設置爲1來表示該方法加鎖,同時它會在常量池中增長這一標識符,獲取它的monitor監視器,本質上是同樣的。優化

經過如下命令輸出一些附加信息後,能夠看到metod二、method3的方法flags上,多了一個ACC_SYNCHRONIZED標籤。
javap -v SynchronizedTest.classthis

public void method1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String this is method1
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 10: 0
        line 11: 8

  public synchronized void method2();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String this is method2
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 13: 0
        line 14: 8

  public static synchronized void method3();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String this is method3
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 17: 0
        line 18: 8

Hotspot 對synchronized的優化歷程

重量級鎖

在jdk1.6版本以前 ,jvm對synchronized實現是須要從用戶態切換到內核態的,jvm會阻塞未獲取到鎖的線程,在鎖被釋放時去喚醒被阻塞的線程。而阻塞和喚醒操做是依賴操做系統來完成的,而且monitor調用的操做系統底層的互斥量(mutex),這會形成很大的開銷,所以稱之爲重量級鎖,這就是synchronized早期實現。spa

自旋鎖和自適應自旋

Java的線程是映射到操做系統的原生線程之上的,若是要阻塞或喚醒一個線程,都須要從用戶態切換到內核態之中,所以線程的狀態轉換須要有消費不少的處理器時間。Synchronized重量級鎖是經過操做系統互斥性實現的,互斥同步對性能最大的影響是阻塞的實現,掛起線程和恢復線程都須要轉入內核態中完成,這些操做會給操做系統帶來很大的壓力。同時,虛擬機的開發團隊也注意到在許多的應用上,共享數據的鎖定狀態中會持續很短的一段時間,爲了這段時間去掛起和恢復線程並不值得。因些引入自旋鎖的概念,就是讓後面請求鎖的那個線程等待一下但不放棄處理的執行時間,看看持有鎖的線程是否很快就會釋放,通常的操做爲讓線程執行一個忙循環(自旋)。操作系統

自旋鎖雖然避免了線程切換的開銷,可是它要佔用處理器時間。所以若是鎖被佔用的時間很是短,那麼使用自旋鎖自旋等待的效果就會很好,相反,若是鎖被佔用的時間很是長,使用自旋鎖就會浪費額外的處理器資源。所以,自旋鎖須要設置一個自旋次數,默認值爲10次,用戶能夠經過jvm參數 -XX:PreBlockSpin來進行修改。

在jdk 1.6引入了自適應的的自旋鎖,自適應代表了自旋的時間再也不固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定的,若是在同一鎖對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機也會認爲此次自旋也可能成功得到到鎖,進而提升自旋的時間。

輕量級鎖

在jdk1.6以後,爲了下降使用synchronized時的性能損耗,引入輕量級鎖的概念,也就是在實際沒有鎖競爭的狀況下,將申請互斥量的這步操做省略。而輕量級的實現原理就是將對象頭的Mark Word中後2個bit設置爲00的標誌位來進行控制,標誌位的設置是經過cas原理來進行操做的。若是當前線程獲取到對象的鎖,那麼會將該標誌位置爲00,同時在當前線程的棧幀中開闢出一塊名爲『鎖記錄』內存空間,用於存儲Mark Word信息的拷貝,而後將對象頭中原來存儲Mark Word的區域經過CAS操做更新爲指向鎖記錄的指針,即pointer to lock record,若是操做成功,說明當前線程獲取鎖成功。

上面說的是輕量級鎖的加鎖過程,它的解鎖過程一樣是經過經過CAS操做來進行操做的,操做過程與加鎖相反,就是將存儲在線程鎖的Mark Word信息從新拷貝到對象頭(java對象頭的結構在後面會進行說明)的Mark Word中,若是拷貝完成,那麼整個同步的過程就完成了,若是替換失敗,那麼說明當前有其餘線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。

注意:輕量級鎖能提高程序同步性能的前提條件是,對於絕大部分鎖,在整個同步週期內都是不存在鎖競爭的。若是沒有鎖競爭,就避免了使用操做系統互斥量的損耗,若是存放鎖競爭,除了互斥量的損耗外,還進行CAS操做的額外開銷,這種狀況性能明顯不如重量級鎖。

偏斜鎖/偏向鎖

爲了進一步優化synchronized的實現,提出了偏向鎖的概念,目的是消耗數據在無競爭狀況下的同步原語,即將全部同步操做所有省略。它的實現與輕量級鎖同樣,經過對象頭的Mark word中的後2個bit的標誌位進行實現,不不一樣的是它的標誌位爲01。若是開啓偏向鎖的,對象頭中Mark word的數據結構將是另外一種實現,它會保存thread ID和epoch,即持有偏向鎖的線程id和偏向時間戳。在未獲取到偏向鎖以前,它的threadId爲0,當前進入同步代碼塊時,將經過cas進行操做標誌位設置爲01,同時將當前線程的id記錄在Mark Word的Thread ID位置中,若是CAS操做成功,持有偏向鎖的線程之後每次進入這個鎖相關的同步代碼塊時,虛擬機均可以再也不進行任務同步操做(locking、unlocking和Mark Word的update)。

若是當另一個線程去嘗試獲取這個鎖時,偏向模式就宣告失結束。根據鎖對象目前是否處理被鎖定的狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲01)或輕量級鎖的狀態,後續的同步操做就如上面的輕量級鎖那樣執行。

注意:偏向鎖能夠提升帶有同步但無競爭的程序性能,它一樣是一個帶有效益權衡性質的優化,若是程序中有大多數的鎖老是被多個不一樣的線程訪問,那使用偏向鎖會形成更多額外的開銷,好比偏向鎖的撤銷,鎖升級。

java對象頭

在Hotspot虛擬機中,對象在內容中存儲的佈局能夠分爲3塊區塊:對象頭,實例數據和對齊填充。而對象頭又包含兩部分信息,第一部分是存儲自身的運行時數據,如哈希碼,GC分代年齡,鎖狀態標誌位,線程持有的鎖、偏向線程ID、偏向時間戳等,這一部分稱之爲"Mark Word"。因爲對象須要存儲的運行時數據不少,但對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計爲一個非固定的數據結構以便在極小的空間內存儲儘可能多的信息,它會根據對象的狀態複用本身的空間。

好比,在不一樣的狀態(未鎖定、輕量級鎖、重量級鎖、GC標記、可偏向/偏向鎖)下的Mark Word的存儲內容將會不同。
image.png

對象頭的另外一部分是指向對象的類元數據的指針,虛擬機經過這個指針來肯定當前對象是哪一個類的實例。並非全部的虛擬機實現都須要在對象數據上保留類型指針,換句話說,查找對象的元數據信息並不必定要經過對象自己(這是由對象訪問定位的方式來決定的:使用句柄和直接指針,hotspot使用後者)。

Mark Word在不一樣狀態下數據結構的變化能夠參考下圖
image.png

完整的對象頭以下圖所示,包含Mark Word和Klass pointer
image.png

本文爲學習周志明老師著做《深刻理解JVM虛擬機》的學習心得。

相關文章
相關標籤/搜索