文章目錄
上篇文章《【併發編程】 — 從字節碼指令的角度去理解synchronized關鍵字的原理》從字節碼指令的角度講解了synchronized關鍵字的原理,從中可以知道其實synchronized關鍵字真正鎖的是鎖對象關聯的monitor對象,那
- (1)這個monitor對象到底什麼呢?
- (2)monitorentor、monitorexit這兩個字節碼指令和monitor之間到底是什麼關係呢?
- (3)爲什麼很多資料上都說monitor是重量級鎖呢?
本篇文章將來講解一下這幾個問題。
1 openjdk(hotspot)源碼下載
源碼地址:http://openjdk.java.net/ --> Mercurial --> jdk8 --> hotspot --> zip
當然如果下載不下來,也可以從我提供的源碼裏clone: https://github.com/nieandsun/concurrent-study.git
2 monitor對象簡介
首先應該知道,monitor並不是我們java開發人員主動創建的一個對象。事實上可以這麼理解:monitor並不是隨着對象的創建而創建的,而是我們通過synchronized修飾符告訴JVM需要爲我們的某個對象(鎖對象)創建關聯的monitor對象。
在HotSpot虛擬機中,monitor是由ObjectMonitor實現的。其源碼是用C++寫的,位於HotSpot虛擬機源碼ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp
)。ObjectMonitor主要數據結構如下:
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;//線程的重入次數
_object = NULL;//存儲改monitor的對象
_owner = NULL;//擁有該monitor的線程
_WaitSet = NULL;//處於wait狀態的線程,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;//多線程競爭鎖時的單向列表
FreeNext = NULL ;
_EntryList = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
對其中幾個比較重要的變量進行解釋如下:
- (1)
_owner:
初始時爲NULL。當有線程佔有該monitor時,owner標記爲該線程的唯一標識。當線程 釋放monitor時,owner又恢復爲NULL。owner是一個臨界資源,JVM是通過CAS操作來保證其線程的安全。 - (2)
_cxq:
競爭隊列,所有請求鎖的線程首先會被放在這個隊列中(單向鏈接)。_cxq是一個臨界資 源,JVM通過CAS原子指令來修改_cxq隊列。修改前_cxq的舊值填入了node的next字段,_cxq指 向新值(新線程)。因此_cxq是一個後進先出的stack(棧)。 - (3)
_EntryList:
_cxq隊列中有資格成爲候選資源的線程會被移動到該隊列中。 - (4)
_WaitSet:
因爲調用wait方法而被阻塞的線程會被放在該隊列中。
鎖對象與monitor中的_owner、_WaitSet以及_EntryList之間的關係可以用下圖進行表示:
3 monitorenter、monitorexit與monitor之間的關係
monitorenter
和 monitorexit
這兩個字節碼指令到底與monitor
之間是什麼樣的關係呢?
其實並沒有那麼不可想象!!! 以monitorenter字節碼指令爲例,當JVM當發現要運行此字節碼指令時,就會調用C++的 InterpreterRuntime::monitorenter函數,該函數的位於HotSpot虛擬機源碼InterpreterRuntime.cpp中(src/share/vm/interpreter/interpreterRuntime.cpp
),具體源碼如下:
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); //JDK1.6及之後對鎖進行的優化
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); //重量級鎖
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
對於重量級鎖,monitorenter函數中會調用 ObjectSynchronizer::slow_enter方法
並最終調用 ObjectMonitor::enter方法(位於:src/share/vm/runtime/objectMonitor.cpp
),源碼如下:
void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;
// 通過CAS操作嘗試把monitor的_owner字段設置爲當前線程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
// 線程重入,recursions++
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
// 如果當前線程是第一次進入該monitor,設置_recursions爲1,_owner爲當前線程
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
//省略一些代碼。。。。
// TODO-FIXME: change the following for(;;) loop to straight-line code.
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
// 如果獲取鎖失敗,則等待鎖的釋放;
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
//
// We have acquired the contended monitor, but while we were
// waiting another thread suspended us. We don't want to enter
// the monitor while suspended because that would surprise the
// thread that suspended us.
//
_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;
jt->java_suspend_self();
}
Self->set_current_pending_monitor(NULL);
}
這裏暫時先不考慮JDK1.6及之後對synchronized關鍵字進行的優化。
以上代碼的具體流程概括起來如下:
- (1) 通過CAS嘗試把monitor的owner字段設置爲當前線程。 ---- 調用了內核函數 Atomic::cmpxchg_ptr
- (2)如果設置之前的owner指向當前線程,說明當前線程再次進入monitor,即重入鎖,執行 recursions ++ ,記錄重入的次數。
- (3)如果當前線程是第一次進入該monitor,設置recursions爲1,_owner爲當前線程,該線程成功獲 得鎖並返回。
- (4) 如果獲取鎖失敗,則等待鎖的釋放。
到這裏我自認爲
作爲java程序員就沒必要再進一步去深究JVM源碼了,因爲從上面的介紹其實已經可以知道,當JVM遇到monitorenter指令時會調用C++的代碼爲當前線程去搶佔鎖對象對應的monitor的所有權。那可以想像當JVM遇到monitorexit指令時肯定也會調用C++的代碼釋放monitor的所有權,並喚醒其他線程(源碼爲src/share/vm/runtime/objectMonitor.cpp
中的ObjectMonitor::exit方法, 有興趣的可以自己去研究)。
做個小圖進行總結一下:
4 爲什麼說monitor是重量級鎖
我這裏並不想去細究什麼是用戶態、內核態或者用戶空間、內核空間 — 有興趣的可以自行百度。
因此這裏只簡單給出兩句話:
-
(1)爭搶鎖+釋放鎖的C++代碼裏會調用一些內核函數。
-
(2)簡單理解就是,JVM調用了內核提供的函數,期間肯定要涉及到cpu從JVM(用戶態)到內核態的來回切換 — 這種切換會帶來大量的系統資源消耗,所以說monitor是一個重量級鎖。
應該知道的事
- (1)JDK1.6之前JVM遇到monitorenter字節碼指令時會直接去搶monitor鎖,也就是說無論被synchronized關鍵字修飾的代碼塊或方法
是否正在併發執行
,都會
涉及到用戶態和內核態的切換 。- 但有研究表明程序大多數情況下都不會遇到併發的情況
— 這就是JDK1.6之前synchronized關鍵字面臨的最大的尷尬,
— 或許這也是Doug Lea
最看不慣的地方 —> 下篇文章將講一下Doug Lea在ReentrantLock中針對該問題的解決方案
- 但有研究表明程序大多數情況下都不會遇到併發的情況
- (2)雖然JDK1.6對synchronized關鍵字進行了優化,但是應該要明確的是monitor鎖仍然存在,也就是說使用synchronized關鍵字有可能最終還是要進行用戶態和內核態的切換
- 其實這個問題是避免不了的,因爲線程掛起(park)和掛起線程的喚醒(unpark)本身就是在進行用戶態和內核態的切換。
- JDK1.6及之後對synchronized關鍵字的優化,本質上是對會出現併發問題的代碼在搶鎖時、在
沒有真正併發執行時
等特殊時機進行的優化,以及鎖的粒度等進行的優化 —》並不是說就杜絕了用戶態和內核態的切換; - 當真正出現併發時,無論是Doug Lea搞得JUC還是synchronized關鍵字都不可避免的要對搶不到鎖的線程進行掛起(park)、對被掛起的線程進行喚醒(unpark)等操作 — 從而會進行用戶態和內核態的切換。
end