天天看點

java thread monitor_java并發系列-monitor機制實作

背景

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

正文

目錄

什麼是monitor

monitor的作用

monitor的組成

尋找monitor鎖

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 classSynchronizedTest {public synchronized voidtest1(){

}public voidtest2(){synchronized (this){

}

}

}

接着我們用javap檢視

java thread monitor_java并發系列-monitor機制實作
java thread monitor_java并發系列-monitor機制實作

從上面可以看出,同步方法jvm是使用ACC_SYNCHRONIZED方法通路辨別符實作同步,同步代碼塊jvm是使用monitorenter和monitorexit指令包裹臨界區實作同步。

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

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

同步方法 文檔

java thread monitor_java并發系列-monitor機制實作
java thread monitor_java并發系列-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 thesynchronized 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 asynchronized 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

java thread monitor_java并發系列-monitor機制實作
java thread monitor_java并發系列-monitor機制實作

The objectref must be of type reference.

Each object is associated with a monitor. A monitor is lockedif and only ifit 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

java thread monitor_java并發系列-monitor機制實作
java thread monitor_java并發系列-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 doso.

該線程減少與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中的每個對象都可以作為鎖。

普通同步方法,鎖是目前執行個體對象。

靜态同步方法,鎖是目前類的class對象。

同步代碼塊,鎖是括号中的對象。

上面的官方文檔也說了每個對象都與一個螢幕關聯。有理由猜測,任意的java對象在執行個體化的時候都同時生成了一個monitor鎖與之一一對應。那麼進一步猜測,通過java對象可以擷取到和它對應的螢幕。

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

4.3.1 對象頭

|-------------------------------------------------------|--------------------|

| 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語言編寫的。

最重要的就是這兩個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

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

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 線程的千裡追蹤

java thread monitor_java并發系列-monitor機制實作
java thread monitor_java并發系列-monitor機制實作

線程通路同步代碼,需要擷取monitor鎖

線程被jvm托管

jvm擷取充當臨界區鎖的java對象

根據java對象對象頭中的重量級鎖 ptr_to_heavyweight_monitor指針找到objectMonitor

将目前線程包裝成一個ObjectWaiter對象

将ObjectWaiter假如_cxq(ContentionList)隊列頭部

_count++

如果owner是其他線程說明目前monitor被占據,則目前線程阻塞。如果沒有被其他線程占據,則将owner設定為目前線程,将線程從等待隊列中删除,count--。

目前線程擷取monitor鎖,如果條件變量不滿足,則将線程放入WaitSet中。當條件滿足之後被喚醒,把線程從WaitSet轉移到EntrySet中。

目前線程臨界區執行完畢

Owner線程會在unlock時,将ContentionList中的部分線程遷移到EntryList中,并指定EntryList中的某個線程為OnDeck線程(一般是最先進去的那個線程)。Owner線程并不直接把鎖傳遞給OnDeck線程,而是把鎖競争的權利交個OnDeck,OnDeck需要重新競争鎖

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