鎖:synchronized原理
1、反彙編方式理解synchronized原理
(1)原始碼
public class Test { private static Object obj = new Object(); public static void main(String[] args) { synchronized (obj) { System.out.println("1"); } } public synchronized void test() { System.out.println("a"); } }
(2)反彙編檢視位元組碼指令
在monitorenter和monitorexit之間執行的是程式碼邏輯
(3)monitorenter
每一個物件都會和一個監視器monitor關聯。監視器被佔用時會被鎖住,其他執行緒無法來獲取該monitor。 當JVM執行某個執行緒的某個方法內部的monitorenter時,它會嘗試去獲取當前物件對應的monitor的所有權。其過程如下:
- 若monior的進入數為0,執行緒可以進入monitor,並將monitor的進入數置為1。當前執行緒成為monitor的owner(所有者)
- 若執行緒已擁有monitor的所有權,允許它重入monitor,則進入monitor的進入數加1
- 若其他執行緒已經佔有monitor的所有權,那麼當前嘗試獲取monitor的所有權的執行緒會被阻塞,直到monitor的進入數變為0,才能重新嘗試獲取monitor的所有權。
synchronized的鎖物件會關聯一個monitor,這個monitor不是我們主動建立的,是JVM的執行緒執行到這個同步程式碼塊,發現鎖物件沒有monitor就會建立monitor,monitor內部有兩個重要的成員變數:owner擁有這把鎖的執行緒,recursions會記錄執行緒擁有鎖的次數,當一個執行緒擁有monitor後其他執行緒只能等待
(4)monitorexit
能執行monitorexit指令的執行緒一定是擁有當前物件的monitor的所有權的執行緒。
執行monitorexit時會將monitor的進入數減1。當monitor的進入數減為0時,當前執行緒退出monitor,不再擁有monitor的所有權,此時其他被這個monitor阻塞的執行緒可以嘗試去獲取這個
2、檢視JVM原始碼
(1)下載原始碼(因為synchronized的原始碼是c++寫的)
選擇版本:
選擇格式:
(2)檢視原始碼
在HotSpot虛擬機器中,monitor是由ObjectMonitor實現的。其原始碼是用c++來實現的,位於HotSpot虛擬機器原始碼ObjectMonitor.hpp檔案中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor主要資料結構如下:
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; }
- _owner:初始時為NULL。當有執行緒佔有該monitor時,owner標記為該執行緒的唯一標識。當執行緒釋放monitor時,owner又恢復為NULL。owner是一個臨界資源,JVM是通過CAS操作來保證其執行緒安全的。
- _cxq:競爭佇列,所有請求鎖的執行緒首先會被放在這個佇列中(單向連結)。_cxq是一個臨界資源,JVM通過CAS原子指令來修改_cxq佇列。修改前_cxq的舊值填入了node的next欄位,_cxq指向新值(新執行緒)。因此_cxq是一個後進先出的stack(棧)。
- _EntryList:_cxq佇列中有資格成為候選資源的執行緒會被移動到該佇列中。
- _WaitSet:因為呼叫wait方法而被阻塞的執行緒會被放在該佇列中。
3、monitor競爭
執行monitorenter時,會呼叫InterpreterRuntime.cpp(位於:src/share/vm/interpreter/interpreterRuntime.cpp) 的 InterpreterRuntime::monitorenter函式。具體程式碼可參見HotSpot原始碼
if (UseBiasedLocking) {//是否用偏向鎖 // Retry fast entry if bias is revoked to avoid unnecessary inflation ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); } 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 ; } // 省略一些程式碼 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); }
- 通過CAS嘗試把monitor的owner欄位設定為當前執行緒。
- 如果設定之前的owner指向當前執行緒,說明當前執行緒再次進入monitor,即重入鎖,執行recursions ++ ,記錄重入的次數。
- 如果當前執行緒是第一次進入該monitor,設定recursions為1,_owner為當前執行緒,該執行緒成功獲得鎖並返回。
- 如果獲取鎖失敗,則等待鎖的釋放
4、monitor等待
競爭失敗等待呼叫的是ObjectMonitor物件的EnterI方法(位於:src/share/vm/runtime/objectMonitor.cpp),原始碼如下所示:
void ATTR ObjectMonitor::EnterI (TRAPS) { Thread * Self = THREAD ; // Try the lock - TATAS if (TryLock (Self) > 0) { assert (_succ != Self , "invariant") ; assert (_owner == Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; } if (TrySpin (Self) > 0) { assert (_owner == Self , "invariant") ; assert (_succ != Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; }//以上程式碼是在沒有獲得鎖的情況下再次嘗試獲取鎖 // 省略部分程式碼 // 當前執行緒被封裝成ObjectWaiter物件node,狀態設定成ObjectWaiter::TS_CXQ; ObjectWaiter node(Self) ; Self->_ParkEvent->reset() ; node._prev = (ObjectWaiter *) 0xBAD ; node.TState = ObjectWaiter::TS_CXQ ; // 通過CAS把node節點push到_cxq列表中 ObjectWaiter * nxt ; for (;;) { node._next = nxt = _cxq ; if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ; // Interference - the CAS failed because _cxq changed. Just retry. // As an optional optimization we retry the lock. if (TryLock (Self) > 0) { assert (_succ != Self , "invariant") ; assert (_owner == Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; } } // 省略部分程式碼 for (;;) { // 執行緒在被掛起前做一下掙扎,看能不能獲取到鎖 if (TryLock (Self) > 0) break ; assert (_owner != Self, "invariant") ; if ((SyncFlags & 2) && _Responsible == NULL) { Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ; } // park self 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 > 1000) RecheckInterval = 1000 ; } else { TEVENT (Inflated enter - park UNTIMED) ; // 通過park將當前執行緒掛起(由於使用者和系統的需要,例如,終端使用者需要暫停程式研究其執行情況或對其進行修改、OS為了提
高記憶體利用率需要將暫時不能執行的程序(處於就緒或阻塞佇列的程序)調出到磁碟),等待被喚醒 Self->_ParkEvent->park() ; } if (TryLock(Self) > 0) break ; // 省略部分程式碼 } // 省略部分程式碼 }
當該執行緒被喚醒時,會從掛起的點繼續執行,通過 ObjectMonitor::TryLock 嘗試獲取鎖,TryLock方法實現如下:
int ObjectMonitor::TryLock (Thread * Self) { for (;;) { void * own = _owner ; if (own != NULL) return 0 ; if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) { // Either guarantee _recursions == 0 or set _recursions = 0. assert (_recursions == 0, "invariant") ; assert (_owner == Self, "invariant") ; // CONSIDER: set or assert that OwnerIsThread == 1 return 1 ; } // The lock had been free momentarily, but we lost the race to the lock. // Interference -- the CAS failed. // We can either return -1 or retry. // Retry doesn't make as much sense because the lock was just acquired. if (true) return -1 ; } }
- 當前執行緒被封裝成ObjectWaiter物件node,狀態設定成ObjectWaiter::TS_CXQ。
- 在for迴圈中,通過CAS把node節點push到_cxq列表中,同一時刻可能有多個執行緒把自己的node節點push到_cxq列表中。
- node節點push到_cxq列表之後,通過自旋嘗試獲取鎖,如果還是沒有獲取到鎖,則通過park將當前執行緒掛起,等待被喚醒。
- 當該執行緒被喚醒時,會從掛起的點繼續執行,通過 ObjectMonitor::TryLock 嘗試獲取鎖。
5、monitor釋放
- 當某個持有鎖的執行緒執行完同步程式碼塊時,會進行鎖的釋放,給其它執行緒機會執行同步程式碼,在HotSpot中,通過退出monitor的方式實現鎖的釋放,並通知被阻塞的執行緒,具體實現位於ObjectMonitor的exit方法中。(位於:src/share/vm/runtime/objectMonitor.cpp)
- 退出同步程式碼塊時會讓_recursions減1,當_recursions的值減為0時,說明執行緒釋放了鎖。
- 根據不同的策略(由QMode指定),從cxq或EntryList中獲取頭節點,通過ObjectMonitor::ExitEpilog 方法喚醒該節點封裝的執行緒,喚醒操作最終由unpark完成
- 被喚醒的執行緒,會回到 void ATTR ObjectMonitor::EnterI (TRAPS) 的第600行,繼續執行monitor的競爭。
6、monitor是重量級鎖
可以看到ObjectMonitor的函式呼叫中會涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等核心函式,執行同步程式碼塊,沒有競爭到鎖的物件會park()被掛起,競爭到鎖的執行緒會unpark()喚醒。這個時候就會存在作業系統使用者態和核心態的轉換,這種切換會消耗大量的系統資源。所以synchronized是Java語言中是一個重量級(Heavyweight)的操作。
核心:可以理解為一種軟體,控制計算機的硬體資源,並提供上層應用程式執行的環境。
使用者空間:上層應用程式活動的空間。應用程式的執行必須依託於核心提供的資源,包括CPU資源、儲存資源、I/O資源等。
系統呼叫:為了使上層應用能夠訪問到這些資源,核心必須為上層應用提供訪問的介面:即系統呼叫。
所有程序初始都運行於使用者空間,此時即為使用者執行狀態(簡稱:使用者態);但是當它呼叫系統呼叫執行某些操作時,例如 I/O呼叫,此時需要陷入核心中執行,我們就稱程序處於核心執行態(或簡稱為核心態)。 系統呼叫的過程可以簡單理解為:
- 使用者態程式將一些資料值放在暫存器中, 或者使用引數建立一個堆疊, 以此表明需要作業系統提供的服務。
- 使用者態程式執行系統呼叫。
- CPU切換到核心態,並跳到位於記憶體指定位置的指令。
- 系統呼叫處理器(system call handler)會讀取程式放入記憶體的資料引數,並執行程式請求的服務。
- 系統呼叫完成後,作業系統會重置CPU為使用者態並返回系統呼叫的結果。
由此可見使用者態切換至核心態需要傳遞許多變數,同時核心還需要保護好使用者態在切換時的一些暫存器值、變數等,以備核心態切換回使用者態。這種切換就帶來了大量的系統資源消耗,這就是在synchronized未優化之前,效率低的原因。