淺談synchronized的實現原理

前言

Synchronized是Java中的重量級鎖,在我剛學Java多線程編程時,我只知道它的實現和monitor有關,可是synchronized和monitor的關係,以及monitor的本質到底是什麼,我並無嘗試理解,而是選擇簡單的略過。在最近的一段時間,因爲實際的須要,我又把這個問題翻出來,Google了不少資料,整個實現的過程總算是弄懂了,爲了以防遺忘,便整理成了這篇博客。 在本篇博客中,我將以class文件爲突破口,試圖解釋Synchronized的實現原理。html

從java代碼的反彙編提及

很容易的想到,能夠從程序的行爲來了解synchronized的實現原理。可是在源代碼層面,彷佛看不出synchronized的實現原理。鎖與不鎖的區別,彷佛僅僅只是有沒有被synchronized修飾。不如把目光放到更加底層的彙編上,看看能不能找到突破口。javap是官方提供的*.class文件分解器,它能幫助咱們獲取*.class文件的彙編代碼。具體用法可參考這裏。 接下來我會使用javap命令對*.class文件進行反彙編。 編寫文件Test.java:java

public class Test {

    private int i = 0;

    public void addI_1(){
        synchronized (this){
            i++;
        }
    }

    public synchronized  void addI_2(){
        i++;
    }
}
複製代碼

生成class文件,並獲取對Test.class反彙編的結果:編程

javac Test.java
javap -v Test.class
複製代碼
Classfile /Users/zhangkunwei/Desktop/Test.class
  Last modified Jul 13, 2018; size 453 bytes
  MD5 checksum ada74ec8231c64230d6ae133fee5dd16
  Compiled from "Test.java"
  ... ...
  public void addI_1();
    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
  ... ...
    public synchronized void addI_2();
    descriptor: ()V
    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
   ... ...
複製代碼

經過反彙編結果,咱們能夠看到:windows

  • 進入被synchronized修飾的語句塊時會執行monitorenter,離開時會執行monitorexit
  • 相較於被synchronized修飾的語句塊,被synchronized修飾的方法中沒有指令monitorentermonitorexit,且flags中多了ACC_SYNCHRONIZED標誌。 monitorentermonitorexit指令是作什麼的?同步語句塊和同步方法的實現原理有何不一樣?遇事不決查文檔,看看官方文檔的解釋。

monitorenter

Description The objectref must be of type reference.bash

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.oracle

  • 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.jvm

Noteside

  • A monitorenter instruction may be used with one or more monitorexit instructions (§monitorexit) to implement a synchronized statement in the Java programming language (§3.14). The monitorenter and monitorexit instructions are not used in the implementation of synchronized methods, although they can be used to provide equivalent locking semantics. Monitor entry on invocation of a synchronized method, and monitor exit on its return, are handled implicitly by the Java Virtual Machine's method invocation and return instructions, as if monitorenter and monitorexit were used.

簡單翻譯一下: 指令monitorenter的操做的必須是一個對象的引用,且其類型爲引用。每個對象都會有一個monitor與之關聯,當且僅當monitor被(其餘(線程)對象)持有時,monitor會被鎖上。其執行細節是,當一個線程嘗試持有某個對象的monitor時:

  • 若是該對象的monitor中的entry count==0,則將entry count置1,並令該線程爲monitor的持有者。
  • 若是該線程已是該對象的monitor的持有者,那麼從新進入monitor,並使得entry count自增一次。
  • 若是其餘線程已經持有該對象的monitor,則該線程將會被阻塞,直到monitor中的entry count==0,而後從新嘗試持有。 注意: monitorenter必須與一個以上monitorexit配合使用來實現Java中的同步語句塊。而同步方法卻不是這樣的:同步方法不使用monitorentermonitorexit來實現。當同步方法被調用時,Monitor介入;當同步方法return時,Monitor退出。這兩個操做,都是被JVM隱式的handle的,就好像這兩個指令被執行了同樣。

monitorexit

Description

  • The objectref must be of type reference.

  • The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.

  • 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.

簡單翻譯一下: 指令monitorenter的操做的必須是一個對象的引用,且其類型爲引用。而且:

  • 執行monitorexit的線程必須是monitor的持有者。
  • 執行monitorexit的線程讓monitorentry count自減一次。若是最後entry count==0,這個線程就再也不是monitor的持有者,意味着其餘被阻塞線程都可以嘗試持有monitor

根據以上信息,上面的疑問獲得瞭解釋:

  1. monitorentermonitorexit是作什麼的? monitorenter能「鎖住」對象。當一個線程獲取monitor的鎖時,其餘請求訪問共享內存空間的線程沒法取得訪問權而被阻塞;monitorexit能「解鎖」對象,喚醒因沒有取得共享內存空間訪問權而被阻塞的線程。

  2. 爲何一個monitorenter與多個monitorexit對應,是一對多,而不是一一對應? 一對多的緣由,是爲了保證:執行monitorenter指令,後面必定會有一個monitorexit指令被執行。上面的例子中,程序正常執行,在離開同步語句塊時執行第一個monitorexit;Runtime期間程序拋出Exception或Error,然後執行第二個monitorexit以離開同步語句塊。

  3. 爲何同步語句塊和同步方法的反彙編代碼略有不一樣? 同步語句塊是使用monitorentermonitorexit實現的;而同步方法是JVM隱式處理的,效果與monitorentermonitorexit同樣。而且,同步方法的flags也不同,多了一個ACC_SYNCHRONIZED標誌,這個標誌是告訴JVM:這個方法是一個同步方法,能夠參考這裏

Monitor

在上一個部分,咱們容易得出一個結論:synchronized的實現和monitor有關。monitor又是什麼呢?從文檔的描述能夠看出,monitor相似於操做系統中的互斥量這個概念:不一樣對象對共享內存空間的訪問是互斥的。在JVMHotspot)中,monitor是由ObjectMonitor實現,其主要的數據結構以下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;   //指向當前monitor的持有者 
    _WaitSet      = NULL;   //持有monitor後,調用的wait()的線程集合
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  //嘗試持有monitor失敗後被阻塞的線程集合
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
}
複製代碼

能夠看出,咱們能夠

  • 經過修改_owner來指明monitor鎖的擁有者;
  • 經過讀取_EntryList來獲取因獲取鎖失敗而被阻塞的線程集合;
  • 經過讀取_WaitSet來獲取在得到鎖後主動放棄鎖的線程集合。

到這裏,synchronized的實現原理已經基本理清楚了,可是還有一個未解決的疑問:線程是怎麼知道monitor的地址的?線程只有知道它的地址,纔可以訪問它,而後才能與以上的分析聯繫上。答案是monitor的地址在Java對象頭中。

Java對象頭

在Java中,每個對象的組成成分中都有一個Java對象頭。經過對象頭,咱們能夠獲取對象的相關信息。 這是Java對象頭的數據結構(32位虛擬機下):

其中的Mark Word,它是一個可變的數據結構,即它的數據結構是依狀況而定的。下面是在對應的鎖狀態下,Mark Word的數據結構(32位虛擬機下):
synchronized是一個重量級鎖,因此對應圖中的重量級鎖狀態。其中有一個字段是:指向重量級鎖的指針,共佔用25+4+1=30bit,它的內容就是這個對象的引用所關聯的 monitor的地址。 線程能夠經過Java對象頭中的Mark Word字段,來獲取 monitor的地址,以便得到鎖。

回到最初的問題

synchronized的實現原理是什麼?從上面的分析來看,答案已經顯而易見了。當多個線程一塊兒訪問共享內存空間時,這些線程能夠經過synchronized鎖住對象的對象頭中,根據Mark Word字段來訪問該對象所關聯的monitor,並嘗試獲取。當一個線程成功獲取monitor後,其餘與之競爭monitor持有權的線程將會被阻塞,並進入EntryList。當該線程操做完畢後,釋放鎖,因爭用monitor失敗而被阻塞的線程就會被喚醒,而後重複以上步驟。

寫在最後

我發現其實大部分答案均可以從文檔中獲得,因此之後遇到問題仍是要嘗試從文檔中找到答案。 本人水平有限,若是本文有錯誤,還望指正,謝謝~

相關文章
相關標籤/搜索