synchronized 原理知多少

synchronized是 Java 編程中的一個重要的關鍵字,也是多線程編程中不可或缺的一員。本文就對它的使用和鎖的一些重要概念進行分析。java

使用及原理

synchronized 是一個重量級鎖,它主要實現同步操做,在 Java 對象鎖中有三種使用方式:編程

  • 普通方法中使用,鎖是當前實例對象。
  • 靜態方法中使用,鎖是當前類的對象。
  • 代碼塊中使用,鎖是代碼代碼塊中配置的對象。

使用

在代碼中使用方法分別以下:數組

普通方法使用:多線程

/** * 公衆號:ytao * 博客:https://ytao.top */
public class SynchronizedMethodDemo{
    public synchronized void demo(){
        // ......
    }
}
複製代碼

靜態方法使用:性能

/** * 公衆號:ytao * 博客:https://ytao.top */
public class SynchronizedMethodDemo{
    public synchronized static void staticDemo(){
        // ......
    }
}
複製代碼

代碼塊中使用:優化

/** * 公衆號:ytao * 博客:https://ytao.top */
public class SynchronizedDemo{
    public void demo(){
        synchronized (SynchronizedDemo.class){
            // ......
        }
    }
}
複製代碼

實現原理

方法和代碼塊的實現原理使用不一樣方式:編碼

代碼塊

每一個對象都擁有一個monitor對象,代碼塊的{}中會插入monitorentermonitorexit指令。當執行monitorenter指令時,會進入monitor對象獲取鎖,當執行monitorexit命令時,會退出monitor對象釋放鎖。同一時刻,只能有一個線程進入在monitorenter中。spa

先將SynchronizedDemo.java使用javac SynchronizedDemo.java命令將其編譯成SynchronizedDemo.class。而後使用javap -c SynchronizedDemo.class反編譯字節碼。線程

Compiled from "SynchronizedDemo.java"
public class SynchronizedDemo {
  public SynchronizedDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void demo();
    Code:
       0: ldc           #2                  // class SynchronizedDemo
       2: dup
       3: astore_1
       4: monitorenter  // 進入 monitor
       5: aload_1
       6: monitorexit  // 退出 monitor
       7: goto          15
      10: astore_2
      11: aload_1
      12: monitorexit  // 退出 monitor
      13: aload_2
      14: athrow
      15: return
    Exception table:
       from    to  target type
           5     7    10   any
          10    13    10   any
}
複製代碼

上面反編碼後的代碼,有兩個monitorexit指令,一個插入在異常位置,一個插入在方法結束位置。設計

方法

方法中的synchronized與代碼塊中實現的方式不一樣,方法中會添加一個叫ACC_SYNCHRONIZED的標誌,當調用方法時,首先會檢查是否有ACC_SYNCHRONIZED標誌,若是存在,則獲取monitor對象,調用monitorentermonitorexit指令。

經過javap -v -c SynchronizedMethodDemo.class命令反編譯SynchronizedMethodDemo類。-v參數即-verbose,表示輸出反編譯的附加信息。下面以反編譯普通方法爲例。

Classfile /E:/SynchronizedMethodDemo.class
  Last modified 2020-6-28; size 381 bytes
  MD5 checksum 55ca2bbd9b6939bbd515c3ad9e59d10c
  Compiled from "SynchronizedMethodDemo.java"
public class SynchronizedMethodDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#13         // java/lang/Object."<init>":()V
   #2 = Fieldref           #14.#15        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #16.#17        // java/io/PrintStream.println:()V
   #4 = Class              #18            // SynchronizedMethodDemo
   #5 = Class              #19            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               demo
  #11 = Utf8               SourceFile
  #12 = Utf8               SynchronizedMethodDemo.java
  #13 = NameAndType        #6:#7          // "<init>":()V
  #14 = Class              #20            // java/lang/System
  #15 = NameAndType        #21:#22        // out:Ljava/io/PrintStream;
  #16 = Class              #23            // java/io/PrintStream
  #17 = NameAndType        #24:#7         // println:()V
  #18 = Utf8               SynchronizedMethodDemo
  #19 = Utf8               java/lang/Object
  #20 = Utf8               java/lang/System
  #21 = Utf8               out
  #22 = Utf8               Ljava/io/PrintStream;
  #23 = Utf8               java/io/PrintStream
  #24 = Utf8               println
{
  public SynchronizedMethodDemo();
    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 5: 0

  public synchronized void demo();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED     // ACC_SYNCHRONIZED 標誌
    Code:
      stack=1, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: invokevirtual #3                  // Method java/io/PrintStream.println:()V
         6: return
      LineNumberTable:
        line 8: 0
        line 10: 6
}
SourceFile: "SynchronizedMethodDemo.java"
複製代碼

上面對代碼塊和方法的實現方式進行探究:

  • 代碼塊經過在編譯後的代碼中添加monitorentermonitorexit指令。
  • 方法中經過添加ACC_SYNCHRONIZED標誌,來決定是否調用monitor對象。

Java 對象頭

synchronized鎖的相關數據存放在 Java 對象頭中。Java 對象頭指的 HotSpot 虛擬機的對象頭,使用2個字寬或3個字寬存儲對象頭。

  • 第一部分存儲運行時的數據,hashCode、鎖標記位、是否偏向鎖、GC分代年齡等等信息,稱做爲Mark Word
  • 第二部分存儲對象類型數據的指針。
  • 第三部分,若是對象是數組的話,則用這部分來存儲數組長度。

Java 對象頭 Mark Word 存儲內容:

存儲內容 標誌位 狀態
對象的hashCode、GC分代年齡 01 無鎖
指向棧中鎖記錄的指針 00 輕量級鎖
指向重量級鎖的指針 10 重量級鎖
11 GC標記
線程ID、Epoch(一個時間戳)、GC分代年齡 01 偏向鎖

鎖升級

synchronized 稱爲重量級鎖,但 Java SE 1.6 爲優化該鎖的性能而減小獲取和釋放鎖的性能消耗,引入偏向鎖輕量級鎖

鎖的高低級別爲:無鎖偏向鎖輕量級鎖重量級鎖

其中鎖的升級是不可逆的,只能由低往高級別升,不能由高往低降。

偏向鎖

偏向鎖是優化在無多線程競爭狀況下,提升程序的的運行性能而使用到的鎖。在Mark Word中存儲一個值,用來標誌是否爲偏向鎖,在 32 位虛擬機和 64 位虛擬機中都是使用一個字節存儲,0 爲非偏向鎖,1 爲是偏向鎖。

當第一次被線程獲取偏向鎖時,會將Mark Word中的偏向鎖標誌設置爲 1,同時使用 CAS 操做來記錄這個線程的ID。獲取到偏向鎖的線程,再次進入獲取鎖時,只需判斷Mark Word是否存儲着當前線程ID,若是是,則不需再次進行獲取鎖操做,而是直接持有該鎖。

撤銷鎖

若是有其餘線程出現,嘗試獲取偏向鎖,讓偏向鎖處於競爭狀態,那麼當前偏向鎖就會撤銷。 撤銷偏向鎖時,首先會暫停持有偏向鎖的線程,並將線程ID設爲空,而後檢查該線程是否存活:

  • 當暫停線程非存活,則設置對象頭爲無鎖狀態。
  • 當暫停線程存活,執行偏向鎖的棧,最後對象頭的保存其餘獲取到偏向鎖的線程ID或者轉向無鎖狀態。

當肯定代碼必定執行在多線程訪問中時,那麼這時的偏向鎖是沒法發揮到優點,若是繼續使用偏向鎖就顯得過於累贅,給系統帶來沒必要要的性能開銷,此時能夠設置 JVM 參數-XX:BiasedLocking=false來關閉偏向鎖。

輕量級鎖

代碼進入同步塊的時候,若是對象頭不是鎖定狀態,JVM 則會在當前線程的棧楨中建立一個鎖記錄的空間,將鎖對象頭的Mark Word複製一份到鎖記錄中,這份複製過來的Mark Word叫作Displaced Mark Word。而後使用 CAS 操做將鎖對象頭中的Mark Word更新爲指向鎖記錄的指針。若是更新成功,當前線程則會得到鎖,若是失敗,JVM 先檢查鎖對象的Mark Word是否指向當前線程,是指向當前線程的話,則當前線程已持有鎖,不然存在多線程競爭,當前線程會經過自旋獲取鎖,這裏的自旋能夠理解爲循環嘗試獲取鎖,因此這過程是消耗 CPU 的過程。當輕量級鎖存在競爭狀態並自旋獲取輕量級鎖失敗時,輕量級鎖就會膨脹爲重量級鎖,鎖對象的Mark Word會更新爲指向重量級鎖的指針,等待獲取鎖的線程進入阻塞狀態。

解鎖

輕量級鎖解鎖是使用 CAS 操做將鎖記錄替換到Mark Word中,若是替換成功,則表示同步操做已完成。若是失敗,則表示其餘競爭線程嘗試過獲取該輕量級鎖,須要在釋放鎖的同時,去喚醒其餘被阻塞的線程,被喚醒的線程回去再次去競爭鎖。

總結

經過分析synchronized的使用以及 Java SE 1.6 升級優化鎖後的設計,能夠看出其主要是解決是經過多加入兩級相對更輕巧的偏向鎖和輕量級鎖來優化重量級鎖的性能消耗,可是這並非必定會起到優化做用,主要是解決大多數狀況下不存在多線程競爭以及同一線程屢次獲取鎖的的優化,這也是根據平時在編碼中多觀察多反思得出的權衡方案。

相關文章
相關標籤/搜索