1. 程式人生 > 實用技巧 >鎖:synchronized原理

鎖: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未優化之前,效率低的原因。