synchronized的工作原理(三)

  • 2020 年 3 月 10 日
  • 筆記

1. synchronized的鎖存儲以及鎖分類

存儲位置: 對象頭的Mark Work

  • JVM的ObjectHeader資訊
    • MarkWord: hashcode(哈希code) + age(分代年齡age) + biased_lock(偏向鎖標誌) + lock (鎖標誌)
    • Class Metadata Address(類元資訊地址)
    • Array Length: 如果對象是一個數組類型,則存儲數組長度
  • 對象的Mark Word資訊
  • JVM中synchronized使用的鎖
    • 無鎖: 嚴格意義上應該說是正常對象,包含hashcode + 分代年齡age + 無偏向鎖標誌 + 鎖狀態標誌
    • 輕量級鎖: 棧記錄的地址 + 鎖狀態
    • 監視器鎖: 對象/監視器地址 + 鎖狀態
    • GC標誌: GC鏈接地址等 + 鎖狀態
    • 偏向鎖(JVM提供的): 當前執行的執行緒ID +支援偏向鎖標記的epoch + 分代年齡age + 偏向鎖標誌 + 鎖狀態
  • JVM源碼關於MarkWord的說明
// markWord.hpp  //  32 bits,32bit的MarkWord存儲資訊如下:  //  hash:25bit --------------->| age:4bit       biased_lock:1bit lock:2bit (normal object)  //  JavaThread*:23bit epoch:2bit age:4bit       biased_lock:1bit lock:2bit (biased object)    //  64 bits, 64bit的MarkWord存儲的資訊資訊如下  //  unused:25bit hash:31bit -->| unused_gap:1   age:4bit    biased_lock:1bit lock:2bit (normal object)  //  JavaThread*:54bit epoch:2bit unused_gap:1   age:4bit    biased_lock:1bit lock:2bit (biased object)    // JVM底層程式碼定義的狀態鎖的值  //    [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    // 當前執行緒持有偏向鎖  //    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread    // 匿名偏向鎖,說明當前執行緒不持有偏向鎖,但是對象已被其他執行緒設置為偏向鎖  //    [0           | epoch | age | 1 | 01]       lock is anonymously biased

jvm底層使用的鎖

// 源碼定義的鎖  class BasicLock			// 輕量級鎖  class BasicObjectLock   // 對象or監視器鎖,重量級鎖

synchronized細節問題說明

  • 偏向鎖: JVM創建對象如果沒有發生競爭,則默認開啟偏向鎖,即偏向鎖標誌+無鎖狀態,如果創建對象多次(JVM經過統計)之後發現處於競爭狀態將會關閉偏向鎖,這是在JVM底層實現的,在JVM源碼中顯示為BiasedLock
  • 輕量級鎖:JVM啟動並創建執行緒的時候,會在棧幀中為執行緒分配執行緒棧空間資訊,此時執行緒棧會開闢一個鎖記錄(Lock Record)空間,並且會將鎖定對象的mark word複製到鎖記錄空間中,jvm稱鎖記錄的mark word為displaced_mark_word,執行緒將通過CAS完成對象的mark word複製操作,成功則獲得鎖,失敗表示有其他鎖競爭,將會通過自旋鎖的方式獲取(不斷CAS循環獲取)
  • 重量級鎖:如果上述的輕量級鎖自旋一定次數之後仍然獲取鎖失敗,便會升級稱為重量級鎖,使用重量級鎖的時候鎖將無法降級,這時候jvm提供使用快速獲取鎖的方式來實現,比如quick_enter/reenter/complete_exit,直接繞過slow-path的路徑,相當於走「捷徑」方法調用
2. synchronized加鎖原理

synchronized的enter加鎖源碼

// synchronizer.hpp  // This is the "slow path" version of monitor enter and exit.  // "slow path": 理解為緩慢路徑,也就是jvm會通過當前方法檢測對象是否處於競爭狀態來確定鎖的升級,以便於加快程式的性能(體現在響應時間和吞吐量)  static void enter(Handle obj, BasicLock* lock, TRAPS);    // 加鎖具體實現:synchronizer.cpp  // 校驗是否開啟偏向鎖,默認是開啟偏向鎖的設置  if (UseBiasedLocking) {      // 撤銷偏向鎖操作      if (!SafepointSynchronize::is_at_safepoint()) {      // Java執行緒執行存在競爭,將當前的obj的markword設置為非偏向鎖狀態        BiasedLocking::revoke(obj, THREAD);      } else {      // is_at_safepoint: 程式的所有Java用戶執行緒都處於停止或者阻塞狀態,除了在VM執行緒和本地執行的Java執行緒可執行      // 阻塞所有Java執行緒,安全撤銷偏向鎖        BiasedLocking::revoke_at_safepoint(obj);      }    }      markWord mark = obj->mark();    assert(!mark.has_bias_pattern(), "should not see bias pattern here");      if (mark.is_neutral()) {      // 設置為輕量級鎖      lock->set_displaced_header(mark);      // CAS自旋鎖,如果失敗將升級為重量級別鎖      if (mark == obj()->cas_set_mark(markWord::from_pointer(lock), mark)) {        return;      }    } else if (mark.has_locker() &&               THREAD->is_lock_owned((address)mark.locker())) {      // 鎖升級失敗,也就是不需要升級鎖,表示當前執行緒已經獲取鎖了      // 不需要再獲取相同的鎖,相當於重入鎖/鎖消除,直接設置對應的執行緒棧幀的displaced_mark_word為null      assert(lock != mark.locker(), "must not re-lock the same lock");      assert(lock != (BasicLock*)obj->mark().value(), "don't relock with same BasicLock");      lock->set_displaced_header(markWord::from_pointer(NULL));      return;    }      // unused_mark : it is only used to be stored into BasicLock as the indicator that the lock is using heavyweight monitor    // 升級為重量級鎖    lock->set_displaced_header(markWord::unused_mark());    // 鎖升級的原因為: inflate_cause_monitor_enter,執行緒發生競爭爭搶鎖    inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);

synchronized偏向鎖的加鎖與撤銷流程

synchronized鎖升級流程(輕量級鎖升級到重量級鎖過程)

3.synchronized解鎖過程

synchronized的exit源碼

// synchronizer.hpp  static void exit(oop obj, BasicLock* lock, Thread* THREAD);    // 具體實現:synchronized.cpp  void ObjectSynchronizer::exit(oop object, BasicLock* lock, TRAPS) {    // 對於偏向鎖的處理,撤銷操作是加鎖的過程,這裡僅僅是對偏向鎖存在的驗證    // 省略對應程式碼 ....      // 對markword的狀態進行診斷判讀,沒有任何其他工作,    // 省略程式碼 .....    // 釋放輕量級鎖,本質上就是一個逆向恢復markword的過程    if (mark == markWord::from_pointer(lock)) {      assert(dhw.is_neutral(), "invariant");      if (object->cas_set_mark(dhw, mark) == mark) {        return;      }    }      // 釋放重量級鎖,需要通過slow path的方式進行釋放鎖    // We have to take the slow-path of possible inflation and then exit.    inflate(THREAD, object, inflate_cause_vm_internal)->exit(true, THREAD);  }

synchronized解鎖的流程

4. 小結:JVM對synchronized的優化策略

鎖優化目的

  • 提升響應時間
  • 增加程式的吞吐量
  • 基於上述程式碼中,jvm底層程式碼存在緩慢路徑加鎖,說明也存在更快速地加鎖方式,避免更多的加鎖處理流程,提供鎖升級的方式來走「捷徑」調用加鎖方法

優化手段

  • 使用偏向鎖,如果使用資源沒有存在競爭狀態,那麼將開啟偏向鎖的方式進行加鎖,通過上述可以看到偏向鎖的流程,並無需消耗過多的資源,僅操作使用資源的markword資訊,適用於單執行緒或者是並發量不多的場景下
  • 輕量級鎖:如果使用資源存在競爭狀態(有一定的並發基礎),那麼jvm底層就會關閉偏向鎖的設置,開啟使用輕量級鎖,通過將在執行緒已經開啟Lock Record記錄使用資源對象的markword資訊,同時將markword的bitfields設置為棧幀引用地址,但是輕量級鎖會出現CAS自旋消耗CPU資源,使用輕量級鎖可以在並發條件下提升響應速度
  • 重量級鎖:執行緒不自旋轉,不消耗CPU資源,jvm底層同時在鎖升級之後會直接使用重量級鎖對應的加鎖和解鎖方式,也就是走「捷徑」方式調用,但是會阻塞執行緒
  • 自旋鎖: 可以看到這個是在使用CAS對使用資源進行循環compare and set的操作,主要是為了防止執行緒在作業系統底層產生阻塞,採用消耗CPU的方式來不斷對使用資源進行CAS操作,但是會存在一些問題,一個是什麼時候獲取鎖是一個未知數,在jvm底層會通過計數器來設置上限,達到上限之後會採用重量級鎖的方式進行加鎖;另一個就是ABA的問題(就是執行緒無法知道使用資源是否被變更,無法辨別前後的使用資源是否一致)
  • 鎖消除:在VM進行JIT編譯時,會通過上下文的掃描來消除不存在共享資源競爭的鎖,避免產生加鎖的耗時操作

感謝花時間閱讀,如果有用歡迎轉發或者點個好看,謝謝!!!