Synchronized的內部實現原理

我們都知道synchronized可以修飾方法和代碼塊,那么這兩者的內部實現原理是相同的嗎?我們來仔細看一下。

修飾方法和代碼塊的不同

首先我們先看一下通過反編譯出的字節碼兩者有何不同。
源代碼如下:

package jvm;

/**
 * Created by ljm on 29/1/2018.
 */
public class ClassCompile {
    synchronized void test(){}
    void test1(){
        synchronized (ClassCompile.class){

        }
    }
}

我們使用命令javap來反編譯:

javap -verbose ClassCompile

得到的字節碼如下(這里只截取與這里所說有關的部分):

synchronized void test();
    descriptor: ()V
    flags: ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Ljvm/ClassCompile;

void test1();
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class jvm/ClassCompile
         2: dup
         3: astore_1
         4: monitorenter
         5: aload_1
         6: monitorexit
         7: goto          15
        10: astore_2
        11: aload_1
        12: monitorexit
        13: aload_2
        14: athrow
        15: return
      Exception table:
         from    to  target type
             5     7    10   any
            10    13    10   any

我們發現兩者在處理上是有不同的。

  • 同步方法,JVM使用ACC_SYNCHRONIZED標識來實現。即JVM通過在方法訪問標識符(flags)中加入ACC_SYNCHRONIZED來實現同步功能。
  • 同步代碼塊,JVM使用monitorentermonitorexit兩個指令實現同步。即JVM為代碼塊的前后真正生成了兩個字節碼指令來實現同步功能的。
    然后我們分別對這兩個做詳細解釋。

同步方法

The Java? Virtual Machine Specification針對同步方法的說明:

Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool’s method_info structure by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.

這段話適合好好讀讀,大致含義如下:

同步方法是隱式的。一個同步方法會在運行時常量池中的method_info結構體中存放ACC_SYNCHRONIZED標識符。當一個線程訪問方法時,會去檢查是否存在ACC_SYNCHRONIZED標識,如果存在,則先要獲得對應的monitor鎖,然后執行方法。當方法執行結束(不管是正常return還是拋出異常)都會釋放對應的monitor鎖。如果此時有其他線程也想要訪問這個方法時,會因得不到monitor鎖而阻塞。當同步方法中拋出異常且方法內沒有捕獲,則在向外拋出時會先釋放已獲得的monitor鎖

第一句話解釋一下:

同步方法是隱式的。一個同步方法會在運行時常量池中的method_info結構體中存放ACC_SYNCHRONIZED標識符。

這句話的解讀是:我們從上面的反編譯結果也可以看到,同步方法會在class文件中的access_flags中存放ACC_SYNCHRONIZED,那這句話為什么說在運行時常量池中呢?
答:我們看看ACC_SYNCHRONIZED在class文件中的位置如下:

ACC_SYNCHRONIZE在class文件中的位置

可以看到ACC_SYNCHRONIZED標識存放在常量池中,而method_info結構體中的access_flags字段是u2的,所以它只是一個指向常量池的標記。而常量池在加載時就會加載到運行時常量池中。所以這里解釋了為什么上面說ACC_SYNCHRONIZED在運行時常量池中,而看class文件是存放在access_flags中的道理。

同步代碼塊

同步代碼塊使用monitorentermonitorexit兩個指令實現同步, The Java? Virtual Machine Specification中有關于這兩個指令的介紹:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

  • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
    If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
  • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

大致含義如下:

每個對象都會與一個monitor相關聯,當某個monitor被擁有之后就會被鎖住,當線程執行到monitorenter指令時,就會去嘗試獲得對應的monitor。步驟如下:

  1. 每個monitor維護著一個記錄著擁有次數的計數器。未被擁有的monitor的該計數器為0,當一個線程獲得monitor(執行monitorenter)后,該計數器自增變為 1 。
    • 當同一個線程再次獲得該monitor的時候,計數器再次自增;
    • 當不同線程想要獲得該monitor的時候,就會被阻塞。
  2. 當同一個線程釋放 monitor(執行monitorexit指令)的時候,計數器再自減。當計數器為0的時候。monitor將被釋放,其他線程便可以獲得monitor。

The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

這段好很好理解,大致含義如下:

當線程執行monitorexit指令時,會去講monitor的計數器減一,如果結果是0,則該線程將不再擁有該monitor。其他線程就可以獲得該monitor了。

總結一下

  • 同步方法和同步代碼塊底層都是通過monitor來實現同步的。
  • 兩者的區別:同步方式是通過方法中的access_flags中設置ACC_SYNCHRONIZED標志來實現;同步代碼塊是通過monitorenter和monitorexit來實現
  • 我們知道了每個對象都與一個monitor相關聯。而monitor可以被線程擁有或釋放。

至此我們還有兩個問題沒有搞清楚:

  1. 到底啥是monitor?
  2. 每個對象是如何與monitor關聯的?

我們下一篇文章細說。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容