java並發系列-monitor機制實現


 背景

  在jdk1.6以前synchronized的java內置鎖不存在 偏向鎖->輕量級鎖->重量級鎖 的鎖膨脹機制,鎖膨脹機制是1.6之后為了優化java線程同步性能而實現的。而1.6之前都是基於monitor機制的重量級鎖。因為java內部對鎖實現的封裝,就算現在我們也只需要了解重量級鎖就可以了。深入了解monitor機制對學習線程同步非常重要。

 

正文

  目錄

  1. 什么是monitor
  2. monitor的作用
  3. monitor的組成
  4. 尋找monitor鎖
  5. java monitor機制的實現

 

什么是monitor 參考

  monitor直譯過來是監視器的意思,專業一點叫管程。monitor是屬於編程語言級別的,它的出現是為了解決操作系統級別關於線程同步原語的使用復雜性,類似於語法糖,對復雜操作進行封裝。而java則基於monitor機制實現了它自己的線程同步機制,就是synchronized內置鎖。

 

monitor的作用

  monitor的作用就是限制同一時刻,只有一個線程能進入monitor框定的臨界區,達到線程互斥,保護臨界區中臨界資源的安全,這稱為線程同步使得程序線程安全。同時作為同步工具,它也提供了管理進程,線程狀態的機制,比如monitor能管理因為線程競爭未能第一時間進入臨界區的其他線程,並提供適時喚醒的功能。

 

monitor的組成

  3.1 monitor對象

    monitor對象是monitor機制的核心,它本質上是jvm用c語言定義的一個數據類型。對應的數據結構保存了線程同步所需的信息,比如保存了被阻塞的線程的列表,還維護了一個基於mutex的鎖,monitor的線程互斥就是通過mutex互斥鎖實現的。

  3.2 臨界區

    臨界區是被synchronized包裹的代碼塊,可能是個代碼塊,也可能是個方法。

  3.3 條件變量

    條件變量和下方wait signal方法的使用有密切關系 。在獲取鎖進入臨界區之后,如果發現條件變量不滿足使用wait方法使線程阻塞,條件變量滿足后signal喚醒被阻塞線程。 tips:當線程被signal喚醒之后,不是從wait那繼續執行的,而是重新while循環一次判斷條件是否成立。參考

  3.4 定義在monitor對象上的wait() signal() signalAll()操作

 

java中monitor的實現

  4.1 首先先看一下synchronized同步代碼塊和同步方法編譯后的字節碼指令文件分別是什么樣子

    源代碼如下 

public class SynchronizedTest {
    public synchronized void test1(){
    }
    public void test2(){
        synchronized (this){
        }
    }
}

 

接着我們用javap查看

  

 

   

 

   

  從上面可以看出,同步方法jvm是使用ACC_SYNCHRONIZED方法訪問標識符實現同步,同步代碼塊jvm是使用monitorenter和monitorexit指令包裹臨界區實現同步。

  

  4.2 線程執行到同步方法處和同步代碼塊monitorenter和monitorexit指令分別發生了什么

    這里需要看jvm的官方文檔,下面三段話要好好讀一讀,monitor的運行邏輯都包含在里面。

    同步方法 文檔

2.11.10. Synchronization
The Java Virtual Machine supports synchronization of both methods and sequences of instructions within a method by a single synchronization construct: the monitor.

Method-level synchronization is performed implicitly, as part of method invocation and return (§2.11.8). A synchronized method is distinguished in the run-time constant pool's method_info structure (§4.6) 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.

Synchronization of sequences of instructions is typically used to encode the synchronized block of the Java programming language. The Java Virtual Machine supplies the monitorenter and monitorexit instructions to support such language constructs. Proper implementation of synchronized blocks requires cooperation from a compiler targeting the Java Virtual Machine (§3.14).

Structured locking is the situation when, during a method invocation, every exit on a given monitor matches a preceding entry on that monitor. Since there is no assurance that all code submitted to the Java Virtual Machine will perform structured locking, implementations of the Java Virtual Machine are permitted but not required to enforce both of the following two rules guaranteeing structured locking. Let T be a thread and M be a monitor. Then:

The number of monitor entries performed by T on M during a method invocation must equal the number of monitor exits performed by T on M during the method invocation whether the method invocation completes normally or abruptly.

At no point during a method invocation may the number of monitor exits performed by T on M since the method invocation exceed the number of monitor entries performed by T on M since the method invocation.

Note that the monitor entry and exit automatically performed by the Java Virtual Machine when invoking a synchronized method are considered to occur during the calling method's invocation.


2.11.10。同步化
Java虛擬機通過單個同步結構(監視器)支持方法和方法中指令序列的同步。

作為方法調用和返回的一部分,方法級同步是隱式執行的(第2.11.8節)。甲synchronized方法是在運行時間常量池中的區分method_info結構(§4.6由)ACC_SYNCHRONIZED標志,這是由方法調用指令進行檢查。調用方法時ACC_SYNCHRONIZED設置為1時,無論方法調用是正常完成還是突然完成,執行線程都將進入監視器,調用方法本身並退出監視器。在執行線程擁有監視器的時間內,沒有其他線程可以進入它。如果在調用synchronized方法期間引發了異常並且該synchronized方法不處理該異常,則在將該異常重新拋出該方法之前,該方法的監視器將自動退出synchronized。

指令序列的同步通常用於對synchronizedJava編程語言的塊進行編碼 。Java虛擬機提供了 monitorenter和monitorexit指令來支持這種語言構造。正確實現synchronized塊需要目標Java虛擬機(第3.14節)的編譯器的配合。

當方法調用期間,給定監視器上的每個出口與該監視器上的先前條目匹配時,就是結構鎖定。由於不能保證提交給Java虛擬機的所有代碼都將執行結構化鎖定,因此允許但不要求強制執行以下兩個保證結構化鎖定的規則的Java虛擬機實現。設 T為線程, M為監視器。然后:

進行監控條目的數量由Ť上中號的方法調用期間必須等於由執行監控退出的數目Ť上中號 是否該方法調用完成正常或突然的方法調用期間。

在一個方法調用期間沒有點可以通過執行監控退出的數目Ť 上中號,因為該方法的調用超過執行監視器條目的數量Ť 上中號,因為該方法調用。

請注意,在調用synchronized方法時,Java虛擬機在調用方法時自動執行的監視器進入和退出 被視為發生。
View Code

 

 

    同步代碼塊指令 文檔

     monitorenter

The objectref must be of type reference.

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.

該objectref的類型必須是reference。

每個對象都與一個監視器關聯。監視器只有在擁有所有者的情況下才被鎖定。執行monitorenter的線程 嘗試獲得與objectref關聯的監視器的所有權,如下所示:

如果與objectref關聯的監視器的條目計數 為零,則線程進入監視器,並將其條目計數設置為1。然后,該線程是監視器的所有者。

如果線程已經擁有與objectref關聯的監視器 ,則它將重新進入監視器,從而增加其條目計數。

如果另一個線程已經擁有與objectref相關聯的監視器 ,則該線程將阻塞,直到該監視器的條目計數為零為止,然后再次嘗試獲取所有權。
View Code

    monitorexit

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.

該線程減少與objectref關聯的監視器的條目計數。結果,如果條目計數的值為零,則線程退出監視器,並且不再是其所有者。其他被阻止進入監視器的線程也可以嘗試這樣做。
View Code

    對比官方文檔描述的同步方法和同步代碼塊指令,其實功能類似。總結如下

      1.同步方法和同步代碼塊都是通過monitor鎖實現的。

      2.兩者的區別:同步方式是通過方法中的access_flags中設置ACC_SYNCHRONIZED標志來實現;同步代碼塊是通過monitorenter和monitorexit指令來實現

      3.每個java對象都會與一個monitor相關聯,可以由線程獲取和釋放。

      4.如果線程沒有獲取到monitor會被阻塞。

      5.monitor通過維護一個計數器來記錄鎖的獲取,重入,釋放情況。

 

    由此可知當線程執行到同步方法發現此方法有ACC_SYNCHRONIZED標志或者執行到monitorenter指令時,會去嘗試獲取monitor鎖。

    那么就會有個疑問,既然線程需要獲取monitor鎖,那么什么是monitor鎖,並且怎么才算獲取monitor鎖

  

  4.3 尋找monitor鎖

    這里先不甩結論,接下來我們一步一步搜尋monitor鎖。

    之前使用synchronized的時候知道,java中的每個對象都可以作為鎖。

    1. 普通同步方法,鎖是當前實例對象。
    2. 靜態同步方法,鎖是當前類的class對象。
    3. 同步代碼塊,鎖是括號中的對象。

    上面的官方文檔也說了每個對象都與一個監視器關聯。有理由猜測,任意的java對象在實例化的時候都同時生成了一個monitor鎖與之一一對應。那么進一步猜測,通過java對象可以獲取到和它對應的監視器。

    這時候涉及到對象頭的知識點。

    4.3.1 對象頭

      對象頭知識參考1 參考2

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|

       每個java對象在內存中由對象頭,實例數據和對齊填充三塊區域組成。其中對象頭存儲了一些增強對象功能的信息,對象頭中的Mark word 記錄了鎖的相關信息。如果此刻該對象鎖升級為重量級鎖,那么其中在對象頭中存儲了指向基於monitor鎖的指針ptr_to_heavyweight_monitor。這個指針指向的就是我們苦苦尋找的鎖。

      既然監視器是指針指向的內存區域,那么這塊內存區域肯定有自己的數據結構,而這個數據結構保存着線程同步的所有信息。

 

   4.3.2 揭開monitor鎖神秘面紗

         詳情參考

         monitor的定義和初始化是有c語言編寫的。

         http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/095e60e7fc8c/src/share/vm/runtime/objectMonitor.hpp 

 

 

 

     最重要的就是這兩個c語言定義的類,objectMonitor就是對象頭中指向的monitor重量級鎖,objectWaiter是對等待線程的封裝,可以用雙向鏈表保存起來。

    下面解釋objectMonitor中屬性的含義

      

_header

定義:

volatile markOop   _header;       // displaced object header word - mark

說明:

  _header是一個markOop類型,markOop就是對象頭中的Mark Word

 _count  定義:
volatile intptr_t  _count;        // reference count to prevent reclaimation/deflation
 // at stop-the-world time. See deflate_idle_monitors().  // _count is approximately |_WaitSet| + |_EntryList|
說明:搶占該鎖的線程數 約等於 WaitSet.size + EntryList.size
 _waiters

 定義:

volatile intptr_t  _waiters;      // number of waiting threads

說明:等待線程數

_recursions   

定義:

volatile intptr_t  _recursions;   // recursion count, 0 for first entry

說明:鎖重入次數

 _object  

定義:

void*     volatile _object;       // backward object pointer - strong root

說明:監視器鎖寄生的對象。鎖不是平白出現的,而是寄托存儲於對象中

_owner 

定義:

void *  volatile _owner;          // pointer to owning thread OR BasicLock

說明:

指向獲得ObjectMonitor對象的線程或基礎鎖

 _WaitSet

定義:

ObjectWaiter * volatile _WaitSet; // LL of threads wait()ing on the monitor

說明:處於wait狀態的線程,被加入到這個linkedList

 _WaitSetLock

定義:

volatile int _WaitSetLock;        // protects Wait Queue - simple spinlock

說明:protects Wait Queue - simple spinlock ,保護WaitSet的一個自旋鎖(monitor大鎖里面的一個小鎖,這個小鎖用來保護_WaitSet更改)

_Responsible 

定義:

Thread * volatile _Responsible

說明:未知 參考:https://www.jianshu.com/p/09de11d71ef8

 _succ

定義:

  Thread * volatile _succ ;          // Heir presumptive thread - used for futile wakeup throttling

說明:當鎖被前一個線程釋放,會指定一個假定繼承者線程,但是它不一定最終獲得鎖。參考:https://www.jianshu.com/p/09de11d71ef8

 _cxq

定義:

  ObjectWaiter * volatile _cxq ;    // LL of recently-arrived threads blocked on entry.
 // The list is actually composed of WaitNodes, acting  // as proxies for Threads.

說明:ContentionList 參考:https://www.jianshu.com/p/09de11d71ef8

 FreeNext  

定義:

ObjectMonitor * FreeNext ;        // Free list linkage

說明:未知

_EntryList   

定義:

ObjectWaiter * volatile _EntryList ;     // Threads blocked on entry or reentry.

說明:未獲取鎖被阻塞或者被wait的線程重新進入被放入entryList中

 _SpinFreq  

定義:

volatile int _SpinFreq ;          // Spin 1-out-of-N attempts: success rate

說明:未知 可能是獲取鎖的成功率

 _SpinClock

定義:

volatile int _SpinClock ;

說明:未知

 OwnerIsThread

定義:

int OwnerIsThread ;               // _owner is (Thread *) vs SP/BasicLock

說明:當前owner是thread還是BasicLock

 _previous_owner_tid

定義:

volatile jlong _previous_owner_tid; // thread id of the previous owner of the monitor

說明:當前owner的線程id

       其實上面的屬性中我們真正需要了解的就幾個。下面大概描述一下。

      4.3.3 線程的千里追蹤

    

        參考

 

 

    1. 線程訪問同步代碼,需要獲取monitor鎖
    2. 線程被jvm托管
    3. jvm獲取充當臨界區鎖的java對象
    4. 根據java對象對象頭中的重量級鎖 ptr_to_heavyweight_monitor指針找到objectMonitor
    5. 將當前線程包裝成一個ObjectWaiter對象
    6. 將ObjectWaiter假如_cxq(ContentionList)隊列頭部
    7. _count++

    8. 如果owner是其他線程說明當前monitor被占據,則當前線程阻塞。如果沒有被其他線程占據,則將owner設置為當前線程,將線程從等待隊列中刪除,count--。
    9. 當前線程獲取monitor鎖,如果條件變量不滿足,則將線程放入WaitSet中。當條件滿足之后被喚醒,把線程從WaitSet轉移到EntrySet中。
    10. 當前線程臨界區執行完畢
    11. Owner線程會在unlock時,將ContentionList中的部分線程遷移到EntryList中,並指定EntryList中的某個線程為OnDeck線程(一般是最先進去的那個線程)。Owner線程並不直接把鎖傳遞給OnDeck線程,而是把鎖競爭的權利交個OnDeck,OnDeck需要重新競爭鎖

     

      大概流程就是這樣的,但是其中還有很多沒有在這篇博客中提及的知識點就不深入了。

    

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM