【併發編程】 --- 從JVM源碼的角度進一步去理解synchronized關鍵字的原理

源碼地址:https://github.com/nieandsun/concurrent-study.git

上篇文章《【併發編程】 — 從字節碼指令的角度去理解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之間的關係

monitorentermonitorexit 這兩個字節碼指令到底與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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章