synchronized背後的“monitor鎖”和Lock的比較

前言
在前面文章 synchronized關鍵字的四種加鎖方式中介紹了四種synchronized的使用和區別,但是效果都是一樣的,今天我們更加深入的看一看synchronized背後的’'monitor"鎖,以及和Lock的區別。

synchronized背後的’'monitor"鎖

synchronized的使用非常簡單,僅需要在方法或者代碼塊中使用synchronized就可以了,使用看起來似乎很簡單的背後是Java團隊背後爲我們做了很多的努力來簡化我們的使用。synchronized如何使用直接影響了程序的效率。

因爲每一個Java對象內部都只有一個鎖,如果使用synchronized使一個線程獲取了某一個對象的鎖之後,其它線程就無法獲取這個對象的鎖了,因爲只有一個鎖,其他線程只能等這個線程釋放這個鎖。

首先我們看synchronized在同步代碼塊中的使用背後的字節碼,源碼如下:

public class TestSynchronized {

    public void insert(Thread thread) {
        synchronized (this) {

        }
    }

    public void insert1(Thread thread) {
        synchronized (TestSynchronized.class) {

        }
    }
}    
  • 這裏有兩個使用同步代碼塊加鎖的方法,我們使用cmd命令切換到類TestSynchronized對應的目錄下,使用編譯命令javac TestSynchronized.java,這時候會出現一個編譯後的字節碼文件TestSynchronized.class,使用命令javap -verbose TestSynchronized.class就可以查看字節碼文件 如下:
Classfile /D:/develop/androidstudio/Forward/app/src/main/java/com/oman/forward/study/TestSynchronized.class
  Last modified 2020-3-29; size 614 bytes
  MD5 checksum a93f484683fe2ce446f688c977d73f1a
  Compiled from "TestSynchronized.java"
public class com.oman.forward.study.TestSynchronized
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#19         // java/lang/Object."<init>":()V
   #2 = Class              #20            // com/oman/forward/study/TestSynchronized
   #3 = Class              #21            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               synMethod
   #9 = Utf8               (Ljava/lang/Thread;)V
  #10 = Utf8               StackMapTable
  #11 = Class              #20            // com/oman/forward/study/TestSynchronized
  #12 = Class              #22            // java/lang/Thread
  #13 = Class              #21            // java/lang/Object
  #14 = Class              #23            // java/lang/Throwable
  #15 = Utf8               synMethod1
  #16 = Utf8               synMethod2
  #17 = Utf8               SourceFile
  #18 = Utf8               TestSynchronized.java
  #19 = NameAndType        #4:#5          // "<init>":()V
  #20 = Utf8               com/oman/forward/study/TestSynchronized
  #21 = Utf8               java/lang/Object
  #22 = Utf8               java/lang/Thread
  #23 = Utf8               java/lang/Throwable
{
  public com.oman.forward.study.TestSynchronized();
    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 8: 0

  public void synMethod(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_0
         1: dup
         2: astore_2
         3: monitorenter
         4: aload_2
         5: monitorexit
         6: goto          14
         9: astore_3
        10: aload_2
        11: monitorexit
        12: aload_3
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     6     9   any
             9    12     9   any
      LineNumberTable:
        line 11: 0
        line 13: 4
        line 14: 14
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ class com/oman/forward/study/TestSynchronized, class java/lang/Thread, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public void synMethod1(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: ldc           #2                  // class com/oman/forward/study/TestSynchronized
         2: dup
         3: astore_2
         4: monitorenter
         5: aload_2
         6: monitorexit
         7: goto          15
        10: astore_3
        11: aload_2
        12: monitorexit
        13: aload_3
        14: athrow
        15: return
      Exception table:
         from    to  target type
             5     7    10   any
            10    13    10   any
      LineNumberTable:
        line 17: 0
        line 19: 5
        line 20: 15
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 10
          locals = [ class com/oman/forward/study/TestSynchronized, class java/lang/Thread, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public void synMethod2(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 24: 0
}
SourceFile: "TestSynchronized.java"
  • 我們重點關注synMethod和synMethod1,他們使用的是同步代碼塊加鎖的方式,我們看到他們裏面比synMethod2普通的方法多了兩個指令,分別是monitorenter 和 monitorexit 指令,而且有兩個monitorexit指令,之所以有兩個monitorexit指令的原因是monitorenter只需要插入到同步代碼塊開始的位置,而monitoreexit需要在同步代碼塊正常結束和異常的位置都插入,這樣就可以在異常的時候也釋放鎖。
  • 每個對象中都有一個維護着被鎖次數的計數器,monitorenter代表計數器加1,monitorexit代表計數器減1,如果計數器的值爲0的話,就代表着這個對象未被線程使用。
  • monitorenter 代表開始加鎖,意味着執行monitorenter的線程開始嘗試獲取對象的monitor的所有權,加鎖分爲幾種情況:
    • 如果對象中monitor的計數器爲0,代表此對象沒有被使用,則執行monitorenter的線程就可以佔有這個對象的monitor。
    • 如果對象中monitor的計數器不爲0,並且此線程已經持有此對象的monitor的話,那麼monitor計數器就累計加1。
    • 如果對象中的monitor計數器不爲0,並且其他線程已經持有了此對象的monitor的話,那麼此線程就處於BLOCKED狀態,需要等待這個monitor計數器的值爲0,值爲0意味着這個monitor已經被釋放了,那麼其它等待這個monitor的線程就可以再次嘗試獲取monitor的所有權了。
  • monitorexit 意味着將對象中的monitor計數器減1,上面分析了,當計數器的值爲0的時候意味着這個monitor已經被釋放了,那麼其它等待這個monitor的線程就可以再次嘗試獲取monitor的所有權了。

接下來我們看synchronized在同步方法中使用背後的字節碼,源碼如下:

public class TestSynchronized {

    public void synMethod2(Thread thread) {

    }

    public synchronized void synMethod3(Thread thread) {

    }

    public static synchronized void synMethod4(Thread thread) {

    }
}
  • 這裏有兩個使用同步方法加鎖的方法,我們還是和上面的命令一樣,使用cmd命令切換到類TestSynchronized對應的目錄下,使用編譯命令javac TestSynchronized.java,這時候會出現一個編譯後的字節碼文件TestSynchronized.class,使用命令javap -verbose TestSynchronized.class就可以查看字節碼文件 如下:
Classfile /D:/develop/androidstudio/Forward/app/src/main/java/com/oman/forward/study/TestSynchronized.class
  Last modified 2020-3-29; size 409 bytes
  MD5 checksum cf108dae480c6b6d58d6cfc8bb7a9c01
  Compiled from "TestSynchronized.java"
public class com.oman.forward.study.TestSynchronized
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#14         // java/lang/Object."<init>":()V
   #2 = Class              #15            // com/oman/forward/study/TestSynchronized
   #3 = Class              #16            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               synMethod2
   #9 = Utf8               (Ljava/lang/Thread;)V
  #10 = Utf8               synMethod3
  #11 = Utf8               synMethod4
  #12 = Utf8               SourceFile
  #13 = Utf8               TestSynchronized.java
  #14 = NameAndType        #4:#5          // "<init>":()V
  #15 = Utf8               com/oman/forward/study/TestSynchronized
  #16 = Utf8               java/lang/Object
{
  public com.oman.forward.study.TestSynchronized();
    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 8: 0

  public void synMethod2(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 24: 0

  public synchronized void synMethod3(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 29: 0

  public static synchronized void synMethod4(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 33: 0
}
SourceFile: "TestSynchronized.java"
  • 我們重點關注synMethod3和synMethod4,我們看到他們兩個和synMethod2的主要區別在於多了一個flag爲ACC_SYNCHRONIZED的標記,這個標記用來標識它是同步方法的。而synMethod3和synMethod4的區別則是synMethod4比synMethod3多了一個ACC_STATIC的靜態標識符。

小結

  • 使用synchronized修飾的方法會有一個ACC_SYNCHRONIZED 標識,當此方法在準備執行的時候,會發現此方法包含ACC_SYNCHRONIZED標記,那麼就需要先獲取monitor鎖(如果是非靜態的話,是對象monitor,如果修飾的是靜態方法則是類monitor),獲取monitor之後才能執行方法體中的內容,方法執行結束後會釋放monitor。
  • 使用synchronized修飾同步代碼塊的話,會有兩個標識,monitorenter和monitorexit。monitorenter 代表開始加鎖,意味着執行monitorenter的線程開始嘗試獲取對象的monitor的所有權,獲取到monitor遇到的幾種情況上面已經詳細分析了,當同步代碼塊執行結束後,就會進入monitorexit釋放鎖。
  • 上面分析得知synchronized如果對於方法加鎖的話,因爲同步的範圍過於大,將會影響程序的性能,所以在編程中可以考慮使用同步代碼塊來替代同步方法,這樣對程序的性能會有提升。

synchronized和Lock的區別

synchronized和Lock都是用來保證線程安全的,下面我們比較它們主要的相同點和不同點。

  • 相同點:
    • synchronized和Lock都是用來保證線程安全的: Java併發編程的三要素包括原子性,可見性,有序性。因爲使用synchronized和Lock在同一個時間,只能有一個線程執行代碼,所以就保證了原子性,可見性和有序性,也就保證了線程安全。
    • synchronized和Lock都是可重入鎖:可重入鎖指的是如果線程獲得了某一個對象的monitor,當再次試圖獲取這個對象的monitor的時候,是不需要先釋放之前的monitor的,上面我們分析synchronized說到monitorenter的時候,說到了monitor計數器可以累加,說明synchronized是可重入鎖,Lock也是可重入鎖。
  • 不同點:
    • 靈活性不同: lock可以使用tryLock(time)等方法,如果獲取不到鎖的時候,可以去做其它的事情。而synchronized只能選擇等待或者異常。
    • 加鎖的顯示和隱式: Lock必須顯示的執行加鎖和解鎖,爲了防止發生死鎖,解鎖一般在finally中執行。而synchronized的加鎖和解鎖是Java虛擬機內部實現的,通過上面的class字節碼分析,本質上也是加鎖和解鎖的操作,並且monitorexit有兩次,所以能夠保證異常釋放鎖,只不過這些內容在代碼上是不像Lock直接體現的。
    • 加鎖解鎖的順序: synchronized的加鎖和解鎖,是按順序出現的,而Lock可以加多個所鎖,但是解鎖不用按照順序,可以把先加的鎖後解。比如下面的代碼:
      	//synchronized 有順序要求
      	synchronized(obj1) {
      		synchronized(obj1) {
      			//do something
      		}	
      	}
      	lock無順序要求
      	lock1.lock();
      	lock2.lock();
      	// do something
      	lock2.unlock();
      	lock1.unlock();
      
    • 被佔有的線程個數:因爲每個對象只有一個monitor,所以synchronized只能被一個線程佔有。而lock沒有這個限制。比如ReentrantReadWriteLock可以同時被多個線程持有讀鎖。
    • 公平性: Lock可以設置公平鎖和非公平鎖,比如ReentrantLock默認是不公平鎖,可以通過構造方法設置公平鎖,而synchronized不能設置。

小結

對synchronized和Lock說了這麼多,如何選擇呢,在我看來有以下幾條建議:

  • 從效率考慮: Java在包java.util.concurrent下爲我們提供了很多的併發類,如果使用的話首先考慮使用這些類(前提是足夠了解這些類),比如考慮ConcurrentHashMap等替換HashMap
  • 安全性上考慮: 因爲synchronized使用簡單,而且虛擬機內置幫我們處理了解鎖的操作,就算是異常情況下也會解鎖,所以從編碼上來看的話,synchronized比Lock更加安全(因爲Lock可能會忘記在finally中解鎖)。
  • 從靈活性上考慮: Lock有更多的API可供更復雜的需求選擇,如果你需要比如超時功能,可以考慮使用Lock。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章