Java併發程式設計之synchronized

AndyandJennifer發表於2018-09-22

該文章屬於《Java併發程式設計》系列文章,如果想了解更多,請點選《Java併發程式設計之總目錄》

前言

上篇文章我們講了volatile關鍵字,我們大致瞭解了其為輕量級的同步機制,現在我們來講講我們關於同步的另一個兄弟synchronized。synchronized作為開發中常用的同步機制,也是我們處理執行緒安全的常用方法。相信大家對其都比較熟悉。但是對於其內部原理與底層程式碼實現大家有可能不是很瞭解,下面我就和大家一起徹底瞭解synchronized的使用方式與底層原理。

執行緒安全的問題

執行緒安全的定義:當多個執行緒訪問某個類時,不管執行時環境採用何種排程方式或者這些執行緒將如何交替執行,並且在主調程式碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那麼這個類就是執行緒安全的。

在具體講解synchronized之前,我們需要了解一下什麼是執行緒安全,為什麼會出現執行緒執行緒不安全的問題。請看下列程式碼:

class ThreadNotSafeDemo {
    private static class Count {
        private int num;
        private void count() {
            for (int i = 1; i <= 10; i++) {
                num += i;
            }
            System.out.println(Thread.currentThread().getName() + "-" + num);
        }
    }
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            Count count = new Count();
            public void run() {
                count.count();
            }
        };
		//建立10個執行緒,
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
    }
}
複製程式碼

上述程式碼中,我們建立Count類,在該類中有一個count()方法,計算從1一直加到10的和,在計算完後輸出當前執行緒的名稱與計算的結果,我們期望執行緒輸出的結果是首項為55且等差為55的等差數列。但是結果並不是我們期望的。具體結果如下圖所示:

輸出結果.png

我們可以看見,執行緒並沒有按照我們之間想的那樣,執行緒按照從Thread-0到Thread-9依次排列,並且Thread-0與Thread-1執行緒輸出的結果是錯誤的。

之所以會出現這樣的情況,是CPU在排程的時候執行緒是可以交替執行的,具體來講是因為當前執行緒Thread-0求和後,(求和後num值為55),在即將執行列印語句時,突然CPU開始排程執行Thread-1去執行count()方法,那麼Thread-0就會停留在即將列印語句的位置,當Thread-1執行計算和後(求和後num值為100),這個時候CPU又開始排程Thread-0執行列印語句。則Thread-1開始暫停,而這個時候num值已經為110了,所以Thread-0列印輸出的結果為110。

執行緒安全的實現方法

上面我們瞭解了之所以會出現執行緒安全的問題,主要原因就是因為存在多條執行緒共同操作共享資料,同時CPU的排程的時候執行緒是可以交替執行的。導致了程式的語義發生改變,所以會出現與我們預期的結果違背的情況。因此為了解決這個問題,在Java中提供了兩種方式來處理這種情況。

互斥同步(悲觀鎖)

互斥同步是指當存在多個執行緒操作共享資料時,需要保證同一時刻有且只有一個執行緒在操作共享資料,其他執行緒必須等到該執行緒處理完資料後再進行。

在Java中最基本的互斥同步就是synchronized(這裡我們討論的是jdk1.6之前,在jdk1.6之後Java團隊對鎖進行了優化,後面文章會具體描述),也就是說當一個共享資料被當前正在訪問的執行緒加上互斥鎖後,在同一個時刻,其他執行緒只能處於等待的狀態,直到當前執行緒處理完畢釋放該鎖。

除了synchronized之外,我們還可以使用java.util.concurrent包下的ReentrantLock來實現同步。

非阻塞式同步(樂觀鎖)

互斥同步主要的問題就是進行執行緒阻塞和喚醒鎖帶來的效能問題,為了解決這效能問題,我們有另一種解決方案,當多個執行緒競爭某個共享資料時,沒有獲得鎖的執行緒不會阻塞,而是不斷的嘗試去獲取鎖,直到成功為止。這種方案的原理就是使用迴圈CAS操作來實現。

synchronized的三種使用方式

瞭解了synchronized的解決的問題,那麼我們繼續來看看在Java中在Java中synchronized的使用情況。

在Java中synchronized主要有三種使用的情況。下面分別列出了這幾種情況

  • 修飾普通的例項方法,對於普通的同步方法,鎖式當前例項物件
  • 修飾靜態方法,對於靜態同步方法,鎖式當前類的Class物件
  • 修飾程式碼塊,對於同步方法塊,鎖是Synchronized配置的物件

證明當前普通的同步方法,鎖式當前例項物件

為了證明普通的同步方法中,鎖是當前物件。請觀察以下程式碼:

class SynchronizedDemo {

    public synchronized void normalMethod() {
        doPrint(5);
    }
 
    public void blockMethod() {//注意,同步塊方法塊中,配置的是當前類的物件
        synchronized (this) {
            doPrint(5);
        }
    }
	//列印當前執行緒資訊與角標值
    private static void doPrint(int index) {
        while (index-- > 0) {
            System.out.println(Thread.currentThread().getName() + "--->" + index);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedDemo demo = new SynchronizedDemo();
        new Thread(() -> demo.normalMethod(), "testNormalMethod").start();
        new Thread(() -> demo.normalMethod(), "testBlockMethod").start();
    }
 }
複製程式碼

在上訴程式碼中,分別建立了兩個方法,normalMethod()與blockMethod()方法,其中normalMethod()方法為普通的同步方法,blockMethod()方法中,是一個同步塊且配置的物件是當前類的物件。在Main()方法中,分別建立兩個執行緒執行兩個不同的方法。

程式輸出結果

輸出結果.png
觀察程式輸出結果,我們可以看到normalMethod方法是由於blockMethod方法執行的,且blockMethod方法是在normalMethod方法執行完成之後在執行的。也就證明了我們的對於普通的同步方法鎖式當前例項物件的結論。

證明對於靜態同步方法,鎖式當前類的Class物件

class SynchronizedDemo {
    public void blockMethod() {
        synchronized (SynchronizedDemo.class) {//注意,同步塊方法塊中,配置的是當前類的Class物件
            doPrint(5);
        }
    }
    public static synchronized void staticMethod() {
        doPrint(5);
    }
    /**
     * 列印當前執行緒資訊
     */
    private static void doPrint(int index) {
        while (index-- > 0) {
            System.out.println(Thread.currentThread().getName() + "--->" + index);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        SynchronizedDemo demo = new SynchronizedDemo();
        new Thread(() -> demo.blockMethod(), "testBlockMethod").start();
        new Thread(() -> demo.staticMethod(), "testStaticMethod").start();
    }

}
複製程式碼

在有了第一個結論的證明後,對於靜態同步方法的鎖物件就不再進行描述了(但是大家要注意一下,同步方法塊中配置的物件是當前類的Class物件)。下面直接給出輸出結果:

TIM截圖20180821140901.png

觀察結果,也很明顯的證明了對於靜態同步方法,鎖式當前類的Class物件的結論

Synchronized的原理

下面文章主要是講解jdk1.6之後Java團隊對鎖進行了優化之後的原理,優化之後涉及到偏向鎖、輕量級鎖、重量級鎖。其中該文章都涉及jdk原始碼,這裡把最新的jdk原始碼分享給大家----->jdk原始碼

在瞭解Synchronized的原理的原理之前,我們需要知道三個知識點第一個是CAS操作,第二個是Java物件頭(其中Synchronized使用的鎖就在物件頭中)第三個是jdk1.6對鎖的優化。在瞭解以上三個知識點後,再去理解其原理就相對輕鬆一點。關於CAS操作已經在上篇文章《Java併發程式設計之Java CAS操作》進行過講解,下面我們來講解關於Java物件頭與鎖優化的知識點。

Java物件的記憶體佈局

在Java虛擬機器中,物件在記憶體的儲存的佈局可以分為3塊區域:物件頭(Header)、例項資料(Instance Data)、對其填充(Padding)。其中虛擬機器中的物件頭包括三部分資訊,分別為"Mark Word"、型別指標、記錄陣列長度的資料(可選),具體情況如下圖所示:

物件儲存結構.png

Java物件頭的組成

  • “Mark Word“:第一部分用於儲存物件自身的執行時資料。如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向鎖ID、偏向鎖時間戳等,這部分的資料在長度32位與64位的虛擬機器中分別為32bit和64bit,官方稱為“Mark Word"。
  • 型別指標:物件頭的另一部分是型別指標,即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。(Java SE 1.6中為了減少獲得鎖和釋放鎖帶來的效能消耗而引入的偏向鎖和輕量級鎖)
  • 記錄陣列長度資料:物件頭剩下的一部分是用於記錄陣列長度的資料(如果當前物件不是陣列,就沒有這一部分資料),如果物件是一個Java陣列,那在物件頭中還必須有一塊用於記錄陣列長度的資料。因為虛擬機器可以通過普通Java物件的後設資料資訊來確定Java物件的大小,但是從陣列中的後設資料中無法確定陣列的大小。

“Mark Word“資料結構

其中關於"Mark Word",因為儲存物件頭資訊是與物件身定義的資料無關的額外的儲存成本,考慮到虛擬機器的空間效率,"Mark Word"被設計成一個被設計成一個非固定的資料結構以便在極小的空間儲存儘量多的資訊。它會根據物件的狀態複用自己的儲存區域。在JVM中,“Mark Word"的實現是在markOop.hpp檔案中的markOopDesc類。通過註釋我們大致瞭解”Mark Word"的結構,具體程式碼如下:

hash:儲存物件的雜湊碼
age:GC分代年齡
biased_lock:偏向鎖標誌
lock:鎖狀態標誌
JavaThread*  當前執行緒
epoch:儲存偏向時間戳

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
// 省略部分程式碼


//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
// 省略部分程式碼

複製程式碼

在上述程式碼中,分成了2種不同位數的作業系統,32位與64位。其中關於當前鎖的狀態標誌markOopDesc類中也進行了詳細的說明,具體程式碼如下:

  enum { locked_value             = 0,//輕量級鎖 對應[00] 
         unlocked_value           = 1,//無鎖狀態  對應[01]
         monitor_value            = 2,//重量級鎖 對應[10]
         marked_value             = 3,//GC標記  對應[11]
         biased_lock_pattern      = 5//是否是偏向鎖  對應[101] 其中biased_lock一個bit位,lock兩個bit位
  };
複製程式碼

那麼根據上述程式碼,我們以32位作業系統為例,可以生成如下兩張表:

在無鎖狀態下,32位JVM的“Mark Word"的預設儲存結構

無鎖狀態.png
在無鎖狀態下,“Mark Word“的32bit空間中,25bit用於儲存物件雜湊碼,4bit用於儲存物件分代年齡,2bit用於儲存鎖標誌**(其中01標識當前執行緒為無鎖狀態)**,1bit固定為0。

在有鎖狀態態下,32位JVM的“Mark Word"的預設儲存結構

有鎖狀態.png
在有鎖的狀態下,23個bit位用於儲存當前執行緒id,2個bit位用於儲存偏向鎖時間戳,4個bit為用於儲存分代年齡(用於GC),1個bit位儲存當前是否是偏向鎖,最後的2bit用於當前鎖的不同狀態。其中00標識當前鎖為輕量級鎖,10標識為重量級鎖,01標識當前鎖為偏向鎖。

synchronized鎖優化

Java SE 1.6為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。下面會對各種鎖進行介紹。

  • 偏向鎖 在大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖,當一個執行緒訪問同步塊,並獲取鎖是,會在物件頭中的“Mark word"和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID。以後該執行緒在進入和退出同步塊時,不需要進行CAS操作來加鎖和解鎖。只需簡單地測試一下物件頭的”Mark Word“裡是否儲存著指向當前執行緒的偏向鎖。如果測試成功,表示執行緒已經獲得了鎖。如果測試失敗,則需要再測試一下“Mark Word”中偏向鎖的標識是否設定成1(表示當前是偏向鎖):如果沒有設定,則使用CAS競爭鎖;如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒。
  • 輕量級鎖 執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。然後執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標。如果成功,當前執行緒獲得鎖,如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖。
  • 重量級鎖 輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到物件頭,如則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖,重量級鎖會導致競爭的執行緒互斥同步。

synchronized底層程式碼實現

在瞭解了上述知識點後,我們來了解一下synchronized底層程式碼實現。從JVM規範中可以看到Synchonized在JVM裡的實現原理,JVM基於進入和退出Monitor物件來實現方法同步和程式碼塊同步,但兩者的實現細節不一樣。程式碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是使用位元組碼同步指令ACC_SYNCHRONIZED來實現的,細節在JVM規範裡並沒有詳細說明。但是方法的同步同樣可以使用這兩個指令來實現。那我們這裡我們就以synchronized程式碼塊底層原理來進行講解。

位元組碼同步指令ACC_SYNCHRONIZED原理:JVM通過使用管程(Monitor)來支援同步,JVM可以從方法常量池的方法表結構中的ACC_SYNCHRONIZED訪問標誌來得知一個方法是否宣告為同步方法,當方法呼叫時,呼叫指令將會檢查方法的ACC_SYNCHRONIZED訪問標誌是否被設定,如果設定了,執行執行緒就要求先成功持有管程(Monitor),然後才能執行方法,最後當方法完成(無論是正常完成還是非正常完成)時釋放管程,在方法執行期間,執行執行緒持有了管程,其他任何執行緒都無法在獲取到同一個管程。

synchronized程式碼塊底層原理

在瞭解 synchronized程式碼塊底層原理之前,我們先了解我們常用的synchronized程式碼塊使用方式。

public class SyncCodeBlock {
   public int i;
   public void syncTask(){
       //同步程式碼庫
       synchronized (this){
           i++;
       }
   }
}
複製程式碼

然後我們通過javap指令反編譯得到位元組碼。

 //===========主要看看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:
      //省略其他位元組碼.......
}
複製程式碼

從上訴程式碼中,我們可以明白當我們宣告synchronized程式碼塊的時候,編譯器會我們生產相應的monitorenter 與monitorexit 指令。當我們的JVM把位元組碼載入到記憶體的時候,會對這兩個指令進行解析。其中關於monitorenter 與monitorenter的指令解析是通過InterpreterRuntime.cpp檔案中的InterpreterRuntime::monitorenterInterpreterRuntime::monitorexit兩個函式分別實現的。

  • InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)
  • InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem)

在瞭解具體的方法實現之間,我們需要了解兩個引數資訊,第一個引數猜都都猜出來,當前執行緒的指標,第二個引數為BasicObjectLock型別的指標,那我們來看看BasicObjectLock到底是什麼東西。

BasicObjectLock

關於BasicObjectLock的類具體宣告是在basicLock.hpp檔案下。

class BasicLock {
  friend class VMStructs;
  friend class JVMCIVMStructs;
 private:
  //指向"Mark Word“也就是我們提到過的markOopDesc的指標。這裡markOop是markOopDesc的別名
  volatile markOop _displaced_header;
 public:
  //獲取"Mark Word“也就是我們提到過的markOopDesc,這裡markOop是markOopDesc的別名
  markOop      displaced_header() const               { return _displaced_header; }
  void         set_displaced_header(markOop header)   { _displaced_header = header; }
  //省略部分程式碼
};

class BasicObjectLock {
  friend class VMStructs;
 private:
  BasicLock _lock; //擁有BasicLock物件
  oop       _obj;                                    
  //省略部分程式碼
};
複製程式碼

從該檔案中,我們知道在BasicLock類中指向"Mark Word“的指標,同時BasicObjectLock 也擁有BasicLock物件,那麼BasicObjectLock 就能訪問”Mark Word“中的內容了。那現在我們再來看上面提到的兩個對應的方法。

InterpreterRuntime::monitorenter方法

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  //省略部分程式碼
  if (UseBiasedLocking) {//判斷是否使用偏向鎖
	//如果是使用偏向鎖,則進入快速獲取,避免不必要的膨脹。
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {//否則直接走輕量級鎖的獲取
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  //省略部分程式碼
複製程式碼

當monitorenter方法執行時,會先判斷當前是否開啟偏向鎖(偏向鎖在Java 6和Java 7裡是預設啟用的,但是它在應用程式啟動幾秒鐘之後才啟用,如有必要可以使用JVM引數來關閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應用程式裡所有的鎖通常情況下處於競爭狀態,可以通過JVM引數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程式預設會進入輕量級鎖狀態),如果沒有開啟會直接走輕量級鎖的獲取,也就是slow_enter()方法。

偏向鎖的獲取

ObjectSynchronizer::fast_enter()方法是在sychronizer.cpp檔案進行宣告的,具體程式碼如下:

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock,
                                    bool attempt_rebias, TRAPS) {
  if (UseBiasedLocking) {//如果使用偏向鎖
    if (!SafepointSynchronize::is_at_safepoint()) {//如果不在安全點,獲取當前偏向鎖的狀態(可能撤銷與重偏向)
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {//如果是撤銷與重偏向直接返回
        return;
      }
    } else {//如果在安全點,有可能會撤銷偏向鎖
      assert(!attempt_rebias, "can not rebias toward VM thread");
      BiasedLocking::revoke_at_safepoint(obj);
    }
   //省略部分程式碼
  }
  slow_enter(obj, lock, THREAD);//如果不使用偏向鎖,則走輕量級鎖的獲取
}
複製程式碼

在該方法中如果當前JVM支援偏向鎖,會需要等待全域性安全點(在這個時間點上沒有正在執行的位元組碼),如果當前不在安全點中,會呼叫revoke_and_rebias()方法來獲取當前偏向鎖的狀態(可能為撤銷或撤銷後重偏向)。如果在安全點,會根據當前偏向鎖的狀態來判斷是否需要撤銷偏向鎖。其中revoke_and_rebias()方法是在biasedLocking.cpp中進行宣告的。

BiasedLocking::revoke_and_rebias()方法

BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
  assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint");
  markOop mark = obj->mark();
  
  //第一步,如果沒有其他執行緒佔用該物件(mark word中執行緒id為0,後三位為101,且不嘗試重偏向)
  //這裡“fast enter()方法"傳入的attempt_rebias為true
  if (mark->is_biased_anonymously() && !attempt_rebias) {
    //一般來講,只有在重新計算物件hashCode的時候才會進入該分支,
    //所以直接用用CAS操作將物件設定為無鎖狀態
    markOop biased_value       = mark;
    markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
    markOop res_mark = obj->cas_set_mark(unbiased_prototype, mark);//cas 操作從新設定偏向鎖的狀態
    if (res_mark == biased_value) {//如果CAS操作失敗,說明存在競爭,偏向鎖為撤銷狀態
      return BIAS_REVOKED;
    }
  } else if (mark->has_bias_pattern()) {
    //第二步,判斷當前偏向鎖是否已經鎖定(不管mark word中執行緒id是否為null),嘗試重偏向
    Klass* k = obj->klass();
    markOop prototype_header = k->prototype_header();
    if (!prototype_header->has_bias_pattern()) {
     //第三步如果有執行緒對該物件進行了全域性鎖定(即同步了靜態方法/屬性),則取消偏向操作
      markOop biased_value       = mark;
      markOop res_mark = obj->cas_set_mark(prototype_header, mark);
      assert(!obj->mark()->has_bias_pattern(), "even if we raced, should still be revoked");
      return BIAS_REVOKED;//偏向鎖為撤銷狀態
    } else if (prototype_header->bias_epoch() != mark->bias_epoch()) {  //第四步,如果偏向鎖時間過期,(這個時候有另一個執行緒通過偏向鎖獲取到了這個物件的鎖)
      if (attempt_rebias) {//第五步,如果偏向鎖開啟,重新通過cas操作更新時間戳與分代年齡。
        assert(THREAD->is_Java_thread(), "");
        markOop biased_value       = mark;
        markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
        markOop res_mark = obj->cas_set_mark(rebiased_prototype, mark);
        if (res_mark == biased_value) {
          return BIAS_REVOKED_AND_REBIASED;//撤銷偏移後重偏向。
        }
      } else {//第六步,如果偏向鎖關閉,通過CAS操作更新分代年齡
        markOop biased_value       = mark;
        markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
        markOop res_mark = obj->cas_set_mark(unbiased_prototype, mark);
        if (res_mark == biased_value) {
          return BIAS_REVOKED;////如果CAS操作失敗,說明存在競爭,偏向鎖為撤銷狀態
        }
      }
    }
  }
 //省略部分程式碼...
}
複製程式碼

偏向鎖的獲取由BiasedLocking::revoke_and_rebias方法實現,主要分為五個步驟

  1. 第一步,判斷當前偏向鎖中"Mark word"中執行緒id是否為null,且attempt_rebias =false。如果滿足條件,嘗試通過CAS操作將當前物件設定為無鎖狀態。如果CAS操作失敗,說明存在競爭,偏向鎖為撤銷狀態。
  2. 第二步,判斷當前偏向鎖是否已經鎖定(不管mark word中執行緒id是否為null),會根據當前條件走第三、第四、第五步。
  3. 第三步,如果有執行緒對該物件進行了全域性鎖定(即同步了靜態方法/屬性),偏向鎖為撤銷狀態。
  4. 第四步,判斷偏向鎖時間是否過期(這個時候有另一個執行緒通過偏向鎖獲取到了這個物件的鎖),接著走第五步、第六步的條件判斷
  5. 第五步,在偏向鎖時間過期的條件下,如果偏向鎖開啟,那麼通過CAS操作更新時間戳與分代年齡、執行緒ID,如果失敗,表明該物件的鎖狀態已經從撤銷偏向到了另一執行緒。當前偏向鎖的狀態為撤銷後重偏向。
  6. 第六步,在偏向鎖時間過期的條件下,如果偏向鎖預設關閉,那麼通過CAS操作更新分代年齡,如果失敗,說明存線上程的競爭,偏向鎖為撤銷狀態。

偏向鎖的撤銷

在上文中我們提到了在呼叫fast_enter()方法時,如果在安全點,這時會根據偏向鎖的狀態來判斷是否需要撤銷偏向鎖,也就是呼叫revoke_at_safepoint()方法。其中該方法也是在biasedLocking.cpp中進行宣告的,具體程式碼如下:

void BiasedLocking::revoke_at_safepoint(Handle h_obj) {
  assert(SafepointSynchronize::is_at_safepoint(), "must only be called while at safepoint");
  oop obj = h_obj();
  HeuristicsResult heuristics = update_heuristics(obj, false);//獲得偏向鎖偏向與撤銷的次數
  if (heuristics == HR_SINGLE_REVOKE) {//如果是一次撤銷
    revoke_bias(obj, false, false, NULL, NULL);
  } else if ((heuristics == HR_BULK_REBIAS) ||//如果是多次撤銷或多次偏向
             (heuristics == HR_BULK_REVOKE)) {
    bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL);
  }
  clean_up_cached_monitor_info();
}
複製程式碼

觀察程式碼我們可以發現,會根據當前偏向鎖偏向與撤銷的次數走不同的方法。這裡我們以revoke_bias()方法為例,來進行講解。具體程式碼如下:

static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias, bool is_bulk, JavaThread* requesting_thread, JavaThread** biased_locker) {
  //省略部分程式碼...
  uint age = mark->age();
  markOop   biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age);
  markOop unbiased_prototype = markOopDesc::prototype()->set_age(age);
  
  JavaThread* biased_thread = mark->biased_locker();
  if (biased_thread == NULL) {//判斷當前偏向鎖中,偏向執行緒id是否為null
    if (!allow_rebias) {//如果不允許重偏向,則使其偏向鎖不可用。
      obj->set_mark(unbiased_prototype);
    }
	//省略部分程式碼...
    return BiasedLocking::BIAS_REVOKED;
  }

 //判斷當前偏向鎖偏向的執行緒是否存在
  bool thread_is_alive = false;
  if (requesting_thread == biased_thread) {
    thread_is_alive = true;
  } else {
    ThreadsListHandle tlh;
    thread_is_alive = tlh.includes(biased_thread);
  }
  if (!thread_is_alive) {//如果當前偏向鎖偏向的執行緒不存活
    if (allow_rebias) {
      obj->set_mark(biased_prototype);//如果允許偏向,則將偏向鎖中的 執行緒id置為null
    } else {
      obj->set_mark(unbiased_prototype);//否則,將偏向鎖設定為無鎖狀態 也就是01
    }
    return BiasedLocking::BIAS_REVOKED;
  }

  //遍歷當前鎖記錄,找到擁有鎖的執行緒,將需要的displaced headers 寫到執行緒堆疊中。
  GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(biased_thread);
  BasicLock* highest_lock = NULL;
  for (int i = 0; i < cached_monitor_info->length(); i++) {
    MonitorInfo* mon_info = cached_monitor_info->at(i);
    if (oopDesc::equals(mon_info->owner(), obj)) {
      markOop mark = markOopDesc::encode((BasicLock*) NULL);
      highest_lock = mon_info->lock();
      highest_lock->set_displaced_header(mark);//將dispalece headers 寫入堆疊中
    } 	
    //省略部分程式碼...
  }
  if (highest_lock != NULL) {//將需要的displaced headers 寫到執行緒堆疊
   //省略部分程式碼...
    highest_lock->set_displaced_header(unbiased_prototype);
   //省略部分程式碼...
    obj->release_set_mark(markOopDesc::encode(highest_lock));
 
  //省略部分程式碼...
  } else {//將物件的頭恢復到未鎖定或無偏狀態
     //省略部分程式碼...
    if (allow_rebias) {
      obj->set_mark(biased_prototype);
    } else {
      // Store the unlocked value into the object's header.
      obj->set_mark(unbiased_prototype);
    }
  }
  //獲取偏向鎖指向的執行緒
  if (biased_locker != NULL) {
    *biased_locker = biased_thread;
  }

  return BiasedLocking::BIAS_REVOKED;
}
複製程式碼

在偏向鎖的撤銷,需要等待全域性全域性點(這個時間點沒有在執行的位元組碼),它會首先暫停擁有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否活著,如果執行緒不處於活動狀態。會更將偏向鎖設定為無鎖狀態,如果執行緒仍然活著,擁有偏向鎖的棧 會被執行,遍歷偏向物件的鎖記錄,棧中的鎖記錄和物件頭的Mark Word要麼重新偏向於其他執行緒,要麼恢復到無鎖或者標記物件不適合作為偏向鎖,最後喚醒暫停的執行緒。

輕量級鎖的獲取

在上文中我們說過當monitorenter指令執行時,如果當前偏向鎖沒有開啟或多個執行緒競爭偏向鎖導致偏向鎖升級為輕量級鎖時,那麼會直接走輕量級的鎖的獲取。在講解輕量級鎖的獲取之前,需要講解一個知識點”Displaced Mark Word"。

輕量級鎖獲與“Displaced Mark Word”

在程式碼進入同步塊,執行輕量級鎖獲取之前,如果此同步物件沒有被鎖定(鎖標誌為01狀態),JVM會在當前執行緒的幀棧中建立一個名為鎖記錄(Lock Record)的空間,用於儲存物件目前的"Mark Word"的拷貝(官方把這份拷貝加了一個Displaced字首,及Displaced Mark Word)。虛擬機器將使用CAS操作嘗試將物件的“Mark word"更新為指向Lock Record的指標,如果這個更新動作成功了,那麼這個現場就擁有了該物件的鎖,及該物件處於輕量級鎖定狀態。關於輕量級鎖的獲取,具體示意圖如下:

輕量級鎖獲取示意圖.png

ObjectSynchronizer::slow_enter()方法

在瞭解了具體的輕量級鎖獲取流程後,我們來檢視具體的實現slow_enter()方法。該方法是在sychronizer.cpp檔案進行宣告的。具體程式碼如下:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {

  markOop mark = obj->mark();//第一步 獲取鎖物件的“mark word"
  
  if (mark->is_neutral()) {//第二步,判斷當前鎖是否是無鎖狀態 後兩位標誌位為01
    //第三步,如果是無鎖狀態,儲存物件目前的“mark word"拷貝,
    //通過CAS嘗試將鎖物件Mark Word更新為指向lock Record物件的指標,
    lock->set_displaced_header(mark);
    if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
      TEVENT(slow_enter: release stacklock); //如果更新成功,表示獲得鎖,則執行同步程式碼,
      return;
    }
  }
  //第四步,如果當前mark處於加鎖狀態,且執行緒幀棧中的owner指向當前鎖,則執行同步程式碼, 
  else if (mark->has_locker() &&
             THREAD->is_lock_owned((address)mark->locker())) {
    lock->set_displaced_header(NULL);
    return;
  }
  //第五步,否則說明有多個執行緒競爭輕量級鎖,輕量級鎖需要膨脹升級為重量級鎖;
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD,
                              obj(),
                              inflate_cause_monitor_enter)->enter(THREAD);
}
複製程式碼

在輕量級鎖的獲取中,主要分為五個步驟,主要步驟如下:

  1. 第一步:markOop mark = obj->mark()方法獲取鎖物件的markOop資料mark。
  2. 第二步:mark->is_neutral()方法判斷mark是否為無鎖狀態:mark的偏向鎖標誌位為 0,鎖標誌位為 01。
  3. 第三步:如果處於無鎖狀態,儲存物件目前的“Mark Word"拷貝,通過CAS嘗試將鎖物件的“Mark Word”更新為指向lock Record物件的指標,如果更新成功,表示競爭到鎖,則執行同步程式碼。
  4. 第四步:如果處於有鎖狀態,且執行緒幀棧中的owner指向當前鎖,則執行同步程式碼,
  5. 第五步:如果都不滿足,否則說明有多個執行緒競爭輕量級鎖,輕量級鎖需要膨脹升級為重量級鎖。

適用情形:假設執行緒A和B同時執行到臨界區if (mark->is_neutral()):

  1. 執行緒AB都把Mark Word複製到各自的lock Record空間中,該資料儲存線上程的棧幀上,是執行緒私有的;
  2. 通過CAS操作保證只有一個執行緒可以把指向棧幀的指標複製到Mark Word,假設此時執行緒A執行成功,並返回繼續執行同步程式碼塊。
  3. 執行緒B執行失敗,退出臨界區,通過ObjectSynchronizer::inflate方法開始膨脹鎖(將輕量級鎖膨脹為重量級鎖)

輕量級鎖的撤銷

在上文中,我們講過當走完同步塊的時候,會執行monitorexit指令,而輕量級鎖的釋放這正是在monitorexit執行的時候,也就是InterpreterRuntime::monitorexit()。

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  if (elem == NULL || h_obj()->is_unlocked()) {
    THROW(vmSymbols::java_lang_IllegalMonitorStateException());
  }
  ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
  // Free entry. This must be done here, since a pending exception might be installed on
  // exit. If it is not cleared, the exception handling code will try to unlock the monitor again.
  elem->set_obj(NULL);
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
複製程式碼

在monitorexit()方法中內部會呼叫slow_exit()方法而slow_exit()方法內部會呼叫fast_exit()方法,我們檢視fast_exit()方法。

void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
  markOop mark = object->mark();
  //省略部分程式碼...
  markOop dhw = lock->displaced_header();//獲取執行緒堆疊中的Displaced Mark Word
  if (dhw == NULL) {//如果執行緒堆疊中的Displaced Mark Word為null
	#ifndef PRODUCT
    if (mark != markOopDesc::INFLATING()) {//如果當前輕量級鎖不是在膨脹為重量級鎖
      //省略部分程式碼...
      if (mark->has_monitor()) {//如果已經為重量級鎖,直接返回
        ObjectMonitor * m = mark->monitor();
        assert(((oop)(m->object()))->mark() == mark, "invariant");
        assert(m->is_entered(THREAD), "invariant");
      }
    }
#endif
    return;
  }
 
  //如果當前執行緒擁有輕量級鎖,那麼通過CAS嘗試把Displaced Mark Word替換到當前鎖物件的Mark Word,
  //如果CAS成功,說明成功的釋放了鎖
  if (mark == (markOop) lock) {
    assert(dhw->is_neutral(), "invariant");
    if (object->cas_set_mark(dhw, mark) == mark) {
      TEVENT(fast_exit: release stack-lock);
      return;
    }
  }

  //如果CAS操作失敗,說明其他執行緒在嘗試獲取輕量級鎖,這個時候需要將輕量級鎖升級為重量級鎖
  ObjectSynchronizer::inflate(THREAD,
                              object,
                              inflate_cause_vm_internal)->exit(true, THREAD);
}
複製程式碼

在偏向鎖的釋放中,會經歷一下幾個步驟。

  1. 獲取執行緒堆疊中的Displaced Mark Word
  2. 如果執行緒堆疊中的Displaced Mark Word為null,如果已經為重量級鎖,直接返回。
  3. 如果當前執行緒擁有輕量級鎖,那麼通過CAS嘗試把Displaced Mark Word替換到當前鎖物件的Mark Word,如果CAS成功,說明成功的釋放了鎖
  4. 如果CAS操作失敗,說明其他執行緒在嘗試獲取輕量級鎖,這個時候需要將輕量級鎖升級為重量級鎖。

重量級鎖的獲取

在上文中我們提到過,在多個執行緒進行輕量級鎖的獲取時或在輕量級鎖撤銷時,有肯能會膨脹為重量級鎖,那現在我們就來看看膨脹的具體過程

ObjectMonitor* ObjectSynchronizer::inflate(Thread * Self,
                                           oop object,
                                           const InflateCause cause) {
  EventJavaMonitorInflate event;

  for (;;) {//開始自旋
    const markOop mark = object->mark();
    // The mark can be in one of the following states:
    // *  Inflated     - just return
    // *  Stack-locked - coerce it to inflated
    // *  INFLATING    - busy wait for conversion to complete
    // *  Neutral      - aggressively inflate the object.
    // *  BIASED       - Illegal.  We should never see this

    //1.如果當前鎖已經為重量級鎖了,直接返回
    if (mark->has_monitor()) {
      ObjectMonitor * inf = mark->monitor();
      return inf;
    }


    //2.如果正在膨脹的過程中,在完成膨脹過程中,其他執行緒必須等待。
    if (mark == markOopDesc::INFLATING()) {
      TEVENT(Inflate: spin while INFLATING);
      ReadStableMark(object);
      continue;
    }

	//3.如果當前為輕量級鎖,迫使其膨脹為重量級鎖
    if (mark->has_locker()) {
      ObjectMonitor * m = omAlloc(Self);
      m->Recycle();
      m->_Responsible  = NULL;
      m->_recursions   = 0;
      m->_SpinDuration = ObjectMonitor::Knob_SpinLimit;   // Consider: maintain by type/class

      markOop cmp = object->cas_set_mark(markOopDesc::INFLATING(), mark);
      if (cmp != mark) {
        omRelease(Self, m, true);
        continue;       // Interference -- just retry
      }

      markOop dmw = mark->displaced_mark_helper();
      assert(dmw->is_neutral(), "invariant");

      m->set_header(dmw);

     
      m->set_owner(mark->locker());
      m->set_object(object);
      // TODO-FIXME: assert BasicLock->dhw != 0.

      // Must preserve store ordering. The monitor state must
      // be stable at the time of publishing the monitor address.
      guarantee(object->mark() == markOopDesc::INFLATING(), "invariant");
      object->release_set_mark(markOopDesc::encode(m));

      // Hopefully the performance counters are allocated on distinct cache lines
      // to avoid false sharing on MP systems ...
      OM_PERFDATA_OP(Inflations, inc());
      TEVENT(Inflate: overwrite stacklock);
      if (log_is_enabled(Debug, monitorinflation)) {
        if (object->is_instance()) {
          ResourceMark rm;
          log_debug(monitorinflation)("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
                                      p2i(object), p2i(object->mark()),
                                      object->klass()->external_name());
        }
      }
      if (event.should_commit()) {
        post_monitor_inflate_event(&event, object, cause);
      }
      return m;
    }

	//4.如果為無鎖狀態,重置監視器狀態
    assert(mark->is_neutral(), "invariant");
    ObjectMonitor * m = omAlloc(Self);
    // prepare m for installation - set monitor to initial state
    m->Recycle();
    m->set_header(mark);
    m->set_owner(NULL);
    m->set_object(object);
    m->_recursions   = 0;
    m->_Responsible  = NULL;
    m->_SpinDuration = ObjectMonitor::Knob_SpinLimit;       // consider: keep metastats by type/class

    if (object->cas_set_mark(markOopDesc::encode(m), mark) != mark) {
      m->set_object(NULL);
      m->set_owner(NULL);
      m->Recycle();
      omRelease(Self, m, true);
      m = NULL;
      continue;
 
    }
  //省略部分程式碼...
   return m ;
}
複製程式碼

在輕量級鎖膨脹為重量級鎖大致可以分為以下幾個過程

  1. 如果當前鎖已經為重量級鎖了,直接返回ObjectMonitor 物件。
  2. 如果正在膨脹的過程中,在完成膨脹過程中,其他執行緒自旋等待。這裡需要注意一點,雖然是自旋操作,但不會一直佔用cpu資源,會通過spin/yield/park方式掛起執行緒。
  3. 如果當前為輕量級鎖,迫使其膨脹為重量級鎖
  4. 如果是無鎖,重置ObjectMonitor 中的狀態。

鎖升級示意圖

在瞭解了偏向鎖、輕量級鎖,與重量級鎖的原理後,現在我們來總結一下整個鎖升級的流程。具體如下圖所示:

偏向鎖獲得和撤銷

偏向鎖獲得和撤銷示意圖.png

輕量級鎖膨脹流程圖

輕量級鎖膨脹示意圖.png

重量級鎖的競爭

在上文中,我們主要介紹了整個鎖升級的流程與原始碼實現。而真正執行緒的等待與競爭我們還沒有詳細描述。下面我們就來講講當鎖膨脹為重量級鎖的時候,整個執行緒的競爭與等待過程。重量級鎖的競爭是在objectMonitor.cpp中ObjectMonitor::enter()方法中實現的。

ObjectMonitor結構

在講解具體的鎖獲取之前,我們需要了解每個鎖物件(這裡指已經升級為重量級鎖的物件)都有一個ObjectMonitor(物件監視器)。也就是說每個執行緒獲取鎖物件都會通過ObjectMonitor。程式碼如下所示:(這裡我省略了一些不必要的屬性。大家只需要看一些關鍵的結構)

class ObjectMonitor {
 public:
  enum {
    OM_OK,                    // 沒有錯誤
    OM_SYSTEM_ERROR,          // 系統錯誤
    OM_ILLEGAL_MONITOR_STATE, // 監視器狀態異常
    OM_INTERRUPTED,           // 當前執行緒已經中斷
    OM_TIMED_OUT              // 執行緒等待超時
  };
  volatile markOop   _header;       // 執行緒幀棧中儲存的 鎖物件的mark word拷貝

 protected:                         // protected for JvmtiRawMonitor
  void *  volatile _owner;          // 指向獲得objectMonitor的執行緒或者 BasicLock物件
  volatile jlong _previous_owner_tid;  // 上一個獲得objectMonitor的執行緒id
  volatile intptr_t  _recursions;   // 同一執行緒重入鎖的次數,如果是0,表示第一次進入
  ObjectWaiter * volatile _EntryList; // 在進入或者重進入阻塞狀態下的執行緒連結串列
                             
 protected:
  ObjectWaiter * volatile _WaitSet; // 處於等待狀態下的執行緒連結串列
  volatile jint  _waiters;          //處於等待狀態下的執行緒個數

複製程式碼

重量級鎖的獲取

在瞭解了ObjectMonitor 類中具體結構後,來看看具體的鎖獲取方法ObjectMonitor::enter(),具體程式碼如下所示:

void ObjectMonitor::enter(TRAPS) {

  Thread * const Self = THREAD;//當前進入enter方法的執行緒
 
 //通過CAS操作嘗試吧monitor的_owner( 指向獲得objectMonitor的執行緒或者 BasicLock物件)設定為當前執行緒
  void * cur = Atomic::cmpxchg(Self, &_owner, (void*)NULL);
  
  if (cur == NULL) {//如果成功,當前執行緒獲取鎖成功,直接執行同步程式碼塊
    assert(_recursions == 0, "invariant");
    assert(_owner == Self, "invariant");
    return;
  }
  
  //如果是同一執行緒,則記錄當前重入的次數(上一步CAS操作不管成功還是失敗,都會返回_owner指向的地址)
  if (cur == Self) {
    _recursions++;
    return;
  }

  //如果之前_owner指向的BasicLock在當前執行緒棧上,說明當前執行緒是第一次進入該monitor,
  //設定_recursions為1,_owner為當前執行緒,該執行緒成功獲得鎖並返回;
  if (Self->is_lock_owned ((address)cur)) {
    assert(_recursions == 0, "internal state error");
    _recursions = 1;
    _owner = Self;
    return;
  }
  //省略部
  分程式碼...
  
  //開始競爭鎖
    for (;;) {
      jt->set_suspend_equivalent();
      EnterI(THREAD);
      if (!ExitSuspendEquivalent(jt)) break;
      _recursions = 0;
      _succ = NULL;
      exit(false, Self);
      jt->java_suspend_self();
    }
    Self->set_current_pending_monitor(NULL);

  }
//省略部分程式碼...
}
複製程式碼

在重量級級鎖的競爭步驟,主要分為以下幾個步驟:

  1. 通過CAS操作嘗試吧monitor的_owner( 指向獲得objectMonitor的執行緒或者 BasicLock物件)設定為當前執行緒,如果CAS操作成功,表示執行緒獲取鎖成功,直接執行同步程式碼塊。
  2. 如果是同一執行緒重入鎖,則記錄當前重入的次數。
  3. 如果2,3步驟都不滿足,則開始競爭鎖,走EnterI()方法。
EnterI()方法實現
void ObjectMonitor::EnterI(TRAPS) {
  Thread * const Self = THREAD;
  //省略部分程式碼...
  
  //把當前執行緒被封裝成ObjectWaiter的node物件,狀態設定成ObjectWaiter::TS_CXQ;
  ObjectWaiter node(Self);
  Self->_ParkEvent->reset();
  node._prev   = (ObjectWaiter *) 0xBAD;
  node.TState  = ObjectWaiter::TS_CXQ;//TS_CXQ:為競爭鎖狀態

 //在for迴圈中,通過CAS把node節點push到_cxq連結串列中;
  ObjectWaiter * nxt;
  for (;;) {
    node._next = nxt = _cxq;
    //如果CAS操作失敗,繼續嘗試,是因為當期_cxq連結串列已經發生改變了
    if (Atomic::cmpxchg(&node, &_cxq, nxt) == nxt) break;
	//有可能在放入_cxq連結串列中時,已經獲取到鎖了,直接返回
    if (TryLock (Self) > 0) {
      assert(_succ != Self, "invariant");
      assert(_owner == Self, "invariant");
      assert(_Responsible != Self, "invariant");
      return;
    }
  }
 //將node節點push到_cxq連結串列之後,通過自旋嘗試獲取鎖
  for (;;) {

    if (TryLock(Self) > 0) break;//嘗試獲取鎖
    assert(_owner != Self, "invariant");

    if ((SyncFlags & 2) && _Responsible == NULL) {
      Atomic::replace_if_null(Self, &_Responsible);
    }
    //判斷執行迴圈的次數,如果執行相應迴圈後,如果還是沒有獲取到鎖,則通過park函式將當前執行緒掛起,等待被喚醒
    if (_Responsible == Self || (SyncFlags & 1)) {
      TEVENT(Inflated enter - park TIMED);
      Self->_ParkEvent->park((jlong) recheckInterval);
      // Increase the recheckInterval, but clamp the value.
      recheckInterval *= 8;
      if (recheckInterval > MAX_RECHECK_INTERVAL) { 其中MAX_RECHECK_INTERVAL為1000
        recheckInterval = MAX_RECHECK_INTERVAL;
      }
    } else {
      TEVENT(Inflated enter - park UNTIMED);
      Self->_ParkEvent->park();
    }
	//省略部分程式碼...
    OrderAccess::fence();
  }
 //省略部分程式碼...
  return;
}
複製程式碼

關於EnterI()方法,可以分為以下步驟:

  1. 把當前執行緒被封裝成ObjectWaiter的node物件,同時將該執行緒狀態設定為TS_CXQ(競爭狀態)
  2. 在for迴圈中,通過CAS把node節點push到_cxq連結串列中,如果CAS操作失敗,繼續嘗試,是因為當期_cxq連結串列已經發生改變了繼續for迴圈,如果成功直接返回。
  3. 將node節點push到_cxq連結串列之後,通過自旋嘗試獲取鎖(TryLock方法獲取鎖),如果迴圈一定次數後,還獲取不到鎖,則通過park函式掛起。(並不會消耗CPU資源)

關於獲取鎖的TryLock方法如下所示:

TryLock方法
int ObjectMonitor::TryLock(Thread * Self) {
  void * own = _owner;
  if (own != NULL) return 0;
  if (Atomic::replace_if_null(Self, &_owner)) {
    return 1;
  }
  return -1;
}
複製程式碼

該函式其實很簡單,就是將鎖中的_owner指標指向當前執行緒,如果成功返回1,反之返回-1。

重量級鎖的釋放

void ObjectMonitor::exit(bool not_suspended, TRAPS) {
  Thread * const Self = THREAD;
  if (THREAD != _owner) {//如果當前鎖物件中的_owner沒有指向當前執行緒
    if (THREAD->is_lock_owned((address) _owner)) {
      //但是_owner指向的BasicLock在當前執行緒棧上,那麼將_owner指向當前執行緒
      assert(_recursions == 0, "invariant");
      _owner = THREAD;
      _recursions = 0;
    } else {
	  //省略部分程式碼...
      return;
    }
  }
  
  //如果當前,執行緒重入鎖的次數,不為0,那麼就重新走ObjectMonitor::exit,直到重入鎖次數為0為止
  if (_recursions != 0) {
    _recursions--;        // this is simple recursive enter
    TEVENT(Inflated exit - recursive);
    return;
  }
  //省略部分程式碼...
  for (;;) {

    if (Knob_ExitPolicy == 0) {
      OrderAccess::release_store(&_owner, (void*)NULL);   //釋放鎖
      OrderAccess::storeload();                        // See if we need to wake a successor
      if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
        TEVENT(Inflated exit - simple egress);
        return;
      }
      TEVENT(Inflated exit - complex egress);
    //省略部分程式碼...
    }
    //省略部分程式碼...
    ObjectWaiter * w = NULL;
    int QMode = Knob_QMode;
	
	//根據QMode的模式判斷,
	
    //如果QMode == 2則直接從_cxq掛起的執行緒中喚醒	
    if (QMode == 2 && _cxq != NULL) {
      w = _cxq;
      ExitEpilog(Self, w);
      return;
	    }
     //省略部分程式碼... 省略的程式碼為根據QMode的不同,不同的喚醒機制
	 }
   } 
   //省略部分程式碼...
}
複製程式碼

重量級鎖的釋放可以分為以下步驟:

  1. 判斷當前鎖物件中的_owner沒有指向當前執行緒,如果_owner指向的BasicLock在當前執行緒棧上,那麼將_owner指向當前執行緒。
  2. 如果當前鎖物件中的_owner指向當前執行緒,則判斷當前執行緒重入鎖的次數,如果不為0,那麼就重新走ObjectMonitor::exit(),直到重入鎖次數為0為止。
  3. 釋放當前鎖,並根據QMode的模式判斷,是否將_cxq中掛起的執行緒喚醒。還是其他操作。

感想

寫了這麼久,終於寫完了~~~ 掌聲在哪裡?

該篇文章主要是根據先關部落格與自己對原始碼的理解,發現其實有很多東西自己還是描述的不是很清楚。主要原因是C++程式碼看的我頭大。個人感覺Java的整個鎖的機制其實涉及到蠻多的東西,自己理解的只是冰山一角,如果大家對程式碼或者文章不理解,請輕噴。我也是看的半懂半懂的。原諒我啦~~~

參考

站在巨人的肩膀上能看的更遠~~~

《深入理解Java虛擬機器:JVM高階特性與最佳實踐》

《Java併發程式設計的藝術》

深入理解Java併發之synchronized實現原理

jdk原始碼剖析二: 物件記憶體佈局、synchronized終極原理

jdk原始碼

相關文章