複習synchronized之底層原理

synchronized是什麼

  • 關鍵字,Java利用鎖機制實現線程同步的一種方式。
  • Java實現線程同步的方式:
    1.顯式鎖(lock,需要自己寫代碼去獲取鎖和釋放鎖);
    2.隱式鎖(synchronized,自動的)。

synchronized的保證的特性

  • 原子性:被synchronized關鍵字包裹起來的方法或者代碼塊可以認爲是原子的。因爲在鎖未釋放之前,這段代碼無法被其他線程訪問到,所以從一個線程觀察另外一個線程的時候,看到的都是一個個原子性的操作。在Java中,synchronized對應着兩個字節碼指令monitorenter和monitorexit。通過monitorenter和monitorexit指令,可以保證被synchronized修飾的代碼在同一時間只能被一個線程訪問,在鎖未釋放之前,無法被其他線程訪問到。
  • 可見性:根據JMM(Java Memory Model,Java內存模型)機制,內存主要分爲主內存和工作內存兩種,線程工作時會從主內存中拷貝一份變量到工作內存中。
    JMM對synchronized做了2條規定:
    1.線程解鎖前,必須把變量的最新值刷新到主內存中。
    2.線程加鎖時,先清空工作內存中的變量值,從主內存中重新獲取最新值到工作內存中。
  • 有序性:synchronized可以保證一定程度的有序性,但其是不能禁止指令重排序的,synchronized 代碼塊裏的非原子操作依舊可能發生指令重排。
    這裏要先說一個概念,as-if-serial語義,其是指不管怎麼重排序(編譯器和處理器爲了提高並行度),單線程程序的執行結果都不能被改變。編譯器和處理器無論如何優化,都必須遵守as-if-serial語義。
    as-if-serial語義保證了單線程中指令重排序是有一些限制的,即無論怎麼重排序,都不能影響到單線程執行的結果。而synchronized保證了這一塊程序在同一時間內只能被同一線程訪問,所以其也算是保證了有序性。

synchronized的作用

它的作用有三點:

  • 確保線程互斥的訪問同步代碼
  • 保證共享爲師的修改及時可見
  • 有效解決指令重排,但是不能禁止(synchronized同步中的代碼,JVM不會輕易優化重排序)

synchronized怎麼用

  1. 修飾方法
//普通方法
//鎖對象
public synchronized void synTest (){
	//TODO
}
//靜態方法
//鎖類
public  class SynClass {
	 public static synchronized void show (){}
}

  1. 修飾代碼塊
public  class SynClass {
	//鎖類
	public static void main(String[] args) {
		synchronized(SynClass.class){
			//TODO
		}
	}

}
public  class SynClass {
	//鎖對象
	Object lock = new Object();
	
	public void show(){
		synchronized(lock){
			//TODO
		}
	}
}


在這裏插入圖片描述

有兩種加鎖方式:

  • 對象鎖
  • 類鎖

在這裏插入圖片描述

synchronized是怎麼實現的

  • Java虛擬機中的同步是基於監視器(Monitor)對象實現的,待會細說。瞭解一個概念先

Java對象頭
在JVM中對象在內存中的佈局分三塊:對象頭、實例變量、對齊填充。
在這裏插入圖片描述

  • 對象頭是實現synchronized的鎖對象的基礎
  • 主要結構是由Mark Word 和 Class Metadata Address 組成
    在這裏插入圖片描述
    在這裏插入圖片描述
    Mark Word默認存儲結構外,還有如下可能變化的結構:在這裏插入圖片描述
    monitor對象存在於每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是爲什麼Java中任意對象可以作爲鎖的原因。

synchronized代碼塊底層原理

  • 定義一個synchronized修飾的同步代碼塊,在代碼塊中操作共享變量i
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 2020-5-5; 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 //退出同步方法
  • monitorenter指令指向同步代碼塊的開始位置
  • monitorexit指令則指明同步代碼塊的結束位置
  • 當執行monitorenter指令時,當前線程試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器爲 0,那線程可以成功取得 monitor,並將計數器值設置爲 1,取鎖成功。
  • 如果當前線程已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor,重入時計數器的值也會加 1。
  • 如果其他線程已經擁有 objectref 的 monitor 的所有權,那麼當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值爲0 。

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 2020-5-5; 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之後,爲了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章