OpenJDK9 Hotspot : synchronized 淺析

前言

網上各路大神總結過各類關於 hotspot jvm synchronized 內部實現,看別人的文章總以爲不過癮,因此有了這篇文章,嘗試再扒一次 synchronized 的「底褲」java

數據結構

在分析源代碼以前須要瞭解相關概念,好比 oop, oopDesc, markOop 等,參考網絡上各類解說或者以前系列文章,這裏重點介紹一下 markOop,ObjectWaiter,ObjectMonitor .etc編程

markOop

每一個 Java Object 在 JVM 內部都有一個 native 的 C++ 對象 oop/oopDesc 與之對應,回顧一下 oopDesc 的類定義(內存佈局)網絡

class oopDesc {
private:
    volatile markOop _mark;
}

_mark 被聲明在 oopDesc 類的頂部,因此這個 _mark 能夠認爲是一個 頭部(就像 TCP/IP 數據包頭部),咱們知道"頭部"通常保存着一些重要的狀態和標誌信息,在 markOop.hpp 文件頭部有一大段註釋說明 markOop 內存佈局數據結構

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

這裏只列出 32 位機器上 markOop 的內存佈局,一樣的 32 bit 在不一樣的 object(normal, biased)以及不一樣的 CMS 垃圾蒐集狀態下有不一樣的解釋,這種緊湊的內存複用技術在 C/C++ 系統編程中隨處可見 app

對於 normal object,32 bit 位分爲 4 個字段,其中和 synchronized 相關的是 biased_lock 和 lockjvm

  • hash,對象的 hash 值oop

  • age,對象的年齡,分代 GC 相關佈局

  • biased_lock,偏向鎖標誌this

  • lock,對象鎖標誌線程

  1. 佔兩比特,用於描述 3 種狀態 locked, unlocked, monitor

//    [ptr             | 00]  locked             ptr points to real header on stack
//    [header      | 0 | 01]  unlocked           regular object header
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)
//    [ptr             | 11]  marked             used by markSweep to mark an object
//                                               not valid at any other time

對於 biased boject,biased_lock 比特位被設置,若是對象被偏向鎖定,擁有該偏向鎖的線程指針被保存在 markOop 的高位

//    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
//    [0           | epoch | age | 1 | 01]       lock is anonymously biased

ObjectWaiter

若是一個線程在等待 object monitor(對象監視器),虛擬機會建立一個 ObjectWaiter 對象,並經過 _next 和 _prev 指針將 ObjectWaiter 掛載到 object monitor 中的等待隊列中

class ObjectWaiter : public StackObject {
public:
    ObjectWaiter * volatile _next;
    ObjectWaiter * volatile _prev;
    Thread* _thread
    ...
}

ObjectMonitor

ObjectMonitor 類是對 對象監視器 的封裝,因爲比較重要(關鍵),objectMonitor.hpp 文件中對它進行了大段註釋

// The ObjectMonitor class implements the heavyweight version of a
// JavaMonitor. The lightweight BasicLock/stack lock version has been
// inflated into an ObjectMonitor. This inflation is typically due to
// contention or use of Object.wait().

從註釋能夠看出 ObjectMonitor 是 JavaMonitor(對象鎖)的一個重量級實現,而偏向鎖和 stack lock(?)是另外一種輕量級實現,當調用 Object.wait() 方法時,輕量級 JavaMonitor 會膨脹(提高)成重量級實現

關鍵字段

_owner

當前擁有該 ObjectMonitor 的線程

_EntryList

由 ObjectWaiter 組成的雙向鏈表,JVM 會從該鏈表中取出一個 ObjectWaiter 並喚醒對應的 JavaThread

_cxq

JVM 爲每一個嘗試進入 synchronized 代碼段的 JavaThread 建立一個 ObjectWaiter 並添加到 _cxq 隊列中

_WaitSet

JVM 爲每一個調用 Object.wait() 方法的線程建立一個 ObjectWaiter 並添加到 _WaitSet 隊列中

synchronized 實現

在進入 synchronized 代碼塊或方法時,javac 會插入一條 monitorenter 字節碼指令,退出時插入一條 monitorexit 指令,咱們仍是以 Zero 解釋器爲例來看看 monitorenter/monitorexit 指令是如何實現的,關於 Zero 解釋器相關概念能夠參考以前的文章

monitorenter

在 bytecodeInterpreter.cpp 中可以找到 monitorenter 對應的 case,大概流程以下:

  1. 獲取方法隱含的 this 參數,即 oop

  2. 獲取對象頭部 markOop(參考上文),判斷是否有偏向標誌(has_bias_pattern),若是沒有轉到 4

  3. 偏向鎖相關的處理邏輯

  4. 嘗試使用輕量級鎖,這裏使用了 CAW(compare and swap,比較和交換)原語來保證線程對 oop 中 markOop
    字段的獨佔寫入,成功寫入的線程當即返回(接着運行),失敗的線程則調用 InterpreterRuntime::monitorenter
    方法(重量級鎖)

至此能夠看出加鎖的順序:偏向鎖 -> 輕量級鎖 -> 重量級鎖

CASE(_monitorenter): {
    oop lockee = STACK_OBJECT(-1);
    ...
    if (entry != NULL) {
        entry->set_obj(lockee);
        ...
        markOop mark = lockee->mark();
        intptr_t hash = (intptr_t) markOopDesc::no_hash;
        if (mark->has_bias_pattern()) {
            // 嘗試使用偏向鎖...
        }

        // 嘗試使用輕量級鎖
        // traditional lightweight locking
        if (!success) {
            markOop displaced = lockee->mark()->set_unlocked();
            entry->lock()->set_displaced_header(displaced);
            bool call_vm = UseHeavyMonitors;
            if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
              // Is it simple recursive case?
              if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
                entry->lock()->set_displaced_header(NULL);
              } else {
                CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
              }
            }
        }
    } else {
        istate->set_msg(more_monitors);
        UPDATE_PC_AND_RETURN(0);
    }
}

咱們先把偏向鎖相關的代碼放一遍,接着看 InterpreterRuntime::monitorenter 方法,爲了使代碼更加清晰,咱們忽略掉斷言和條件編譯,

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread,   
        BasicObjectLock* elem))
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  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);
  }
IRT_END

看來 JVM 仍是不死心,這裏又有兩個分支 fast_enter 和 slow_enter,因爲一路上咱們都是挑着最慢的路徑走,這回也不例外,接着扒 slow_enter 方法

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();

  if (mark->is_neutral()) {
    lock->set_displaced_header(mark);
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT(slow_enter: release stacklock);
      return;
    }
    // Fall through to inflate() ...
  } else if (mark->has_locker() &&
             THREAD->is_lock_owned((address)mark->locker())) {
    lock->set_displaced_header(NULL);
    return;
  }

  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD,
                              obj(),
                              inflate_cause_monitor_enter)->enter(THREAD);
}

再次經過 cmpxchg 嘗試輕量級鎖,不然調用 ObjectSynchronizer:: inflate 方法膨脹成重量級鎖(ObjectMonitor)並調用其 enter 方法

ObjectMonitor::enter

ObjectMonitor 對象有一個 _owner 字段代表當前哪一個線程持有 ObjectMonitor,enter 方法首先經過 cmpxchg 嘗試將 _owner 原子性設置成當前線程,若是成功就直接返回,這樣能夠避免進行內核線程的上下文切換

總結

相關文章
相關標籤/搜索