Synchronized之輕量級鎖自旋騙局

語言: CN / TW / HK

之前筆者分析了 synchronized的偏向鎖原始碼,我們今天繼續來看synchronized的輕量級鎖邏輯。關於輕量級鎖,網上有很多說法都是輕量級鎖在發生競爭時會進行自旋,但是經過筆者對原始碼的學習,並沒有發現輕量級鎖的自旋邏輯。筆者甚至去jdk6和jdk15中都進行了一番搜尋,發現也不存在自旋的邏輯,關於輕量級鎖的自旋這個說法,筆者曾經也深信不疑,但是從實際原始碼中出發,並不存在自旋的邏輯。本篇部落格筆者會把整個輕量級鎖部分的原始碼拿出來進行分析,希望能幫助大家以後正確的理解synchronized輕量級鎖。

一.偏向鎖撤銷

我們都知道 synchronized中的輕量級鎖是由偏向鎖升級而來的(關於偏向鎖的原始碼在筆者之前的部落格: http://my.oschina.net/u/3645114/blog/5306767 中)。所以輕量級鎖原始碼的入口必然在偏向鎖後面,事實上再偏向鎖升級成輕量級鎖之前,還需要進行偏向鎖的撤銷,偏向鎖加鎖的方法在interp_masm_x86_64.cpp的lock_object方法中,我們就從這裡開始,話不多說,直接看程式碼:

void InterpreterMacroAssembler::lock_object(Register lock_reg) {
  if (UseHeavyMonitors) {
    call_VM(noreg,
            CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
            lock_reg);
  } else {
    Label done;

    const Register swap_reg = rax; // Must use rax for cmpxchg instruction
    const Register obj_reg = c_rarg3; // Will contain the oop

    const int obj_offset = BasicObjectLock::obj_offset_in_bytes();
    const int lock_offset = BasicObjectLock::lock_offset_in_bytes ();
    const int mark_offset = lock_offset +
                            BasicLock::displaced_header_offset_in_bytes();

    Label slow_case;

    movptr(obj_reg, Address(lock_reg, obj_offset));
    //這裡是偏向鎖邏輯,關於偏向鎖的這部分邏輯筆者之前的部落格已經分析過了,這裡就不進行分析了
    if (UseBiasedLocking) {
      biased_locking_enter(lock_reg, obj_reg, swap_reg, rscratch1, false, done, &slow_case);
    }
    // 之前的部落格提到過撤銷偏向和物件非偏向模式(即已經是輕量級鎖)會走這裡——升級輕量級鎖
    // 這裡偏向鎖的撤銷(Revoke) 操作並不是將物件恢復到無鎖可偏向的狀態
    // 而是指在獲取偏向鎖的過程因為不滿足條件導致要將鎖物件改為非偏向鎖狀態
    // 偏向鎖的撤銷,是輕量級鎖的前提。
    // 將1寫入swap_reg暫存器
    movl(swap_reg, 1);

    //將物件的markword 與1或運算並存入swap_reg暫存器
    //此處鎖標記位是 01 即無鎖,因為執行到這裡只有撤銷偏向鎖和物件非偏向鎖兩種情況
    //這兩種情況下都為00 | 01,即得無鎖
    orptr(swap_reg, Address(obj_reg, 0));

    //將swap_reg中的資料存到lockrecord中的markword位置
    //這裡將lockrecord中的displaced header(本質是一個markword)設定為無鎖的原因是
    //等到解鎖時會將displaced header中的markword替換回物件頭上
    //這時物件應該是無鎖的.
    movptr(Address(lock_reg, mark_offset), swap_reg);

    //彙編lock指令
    if (os::is_MP()) lock();
    //比較交換(cas) 將物件頭替換成指向lockrecord的指標
    cmpxchgptr(lock_reg, Address(obj_reg, 0));

    //成功則表示已經是輕量級鎖且加鎖成功直接結束,失敗則證明有競爭(可能是偏向鎖競爭也可能是輕量級鎖競爭)
    //進入slow_case邏輯
    jcc(Assembler::zero, done);

    ......

    bind(slow_case);

    //slow_case邏輯
    call_VM(noreg,
            CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
            lock_reg);

    bind(done);
  }
}

關於slow_case,筆者之前的部落格也提到過,其實是隻要發生鎖的競爭,就會進入到slow_case部分,我們可以看到,這裡的競爭不只是偏向鎖競爭,也包括輕量級鎖競爭。

還有一點需要注意的,從程式碼中我們可以看到,持有輕量級鎖的lockRecord中的displaced header,即lockRecord中的鎖物件(關於這個鎖物件,筆者之前的部落格也提到過,這裡就不展開) 按照許多網上的說法,這裡應該是儲存物件的markword,而輕量級鎖物件的markword鎖標記位應該是00,但實際上原始碼在這裡將其還原成了無所狀態儲存到了lockRecord,原因就如筆者註釋所說——為了方便解鎖時替換回物件的markword

我們繼續看原始碼:call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),lock_reg) 這個方法(這裡是模板直譯器呼叫了c++的方法,後面的原始碼都是c++相對來說比較好理解一些)可以理解為是呼叫 InterpreterRuntime::monitorenter方法,其引數為lock_reg暫存器,即之前找到的lockRecord:

//呼叫了interpreterRuntime.cpp下的方法
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  ......
  //elem是傳入的lockRecord,將執行緒和lockRecord中的obj封裝成控制代碼
  Handle h_obj(thread, elem->obj());
  //判斷jvm偏向鎖引數
  if (UseBiasedLocking) {
    //會先進入快速處理方法,引數是剛剛封裝的控制代碼和lockRecord中的鎖物件
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  ......
IRT_END

//繼續看ObjectSynchronizer::fast_enter方法
//attempt_rebias引數表示是否接受重偏向,這裡是true
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 if (UseBiasedLocking) {
    //判斷全域性安全點
    if (!SafepointSynchronize::is_at_safepoint()) {
      //撤銷和重偏向方法
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
      //如果是撤銷後重偏向則直接直接返回即還是偏向鎖,沒有重偏向則證明只有撤銷,需要進入輕量級鎖競爭邏輯
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
      }
    } else {
      //安全點撤銷
      BiasedLocking::revoke_at_safepoint(obj);
    }
 }
 //輕量級鎖競爭邏輯
 slow_enter (obj, lock, THREAD) ;
}

到這裡我們可以看到會先執行 偏向鎖的撤銷和重偏向邏輯 ,然後會根據結果判斷是否進入 輕量級鎖競爭邏輯 。我們一步一步來,先看偏向鎖的撤銷和重偏向邏輯,而偏向鎖的撤銷和重偏向邏輯又分為兩個分支,一個是在全域性安全點,一個是在非全域性安全點。這兩個方法其實邏輯是差不多的,我們重點分析非全域性安全點的方法:

//撤銷偏向鎖 attempt_rebias引數表示是否會重偏向這裡傳入的是true
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
 
  markOop mark = obj->mark();
  //判斷是偏向模式,但是尚未偏向其他執行緒,這裡attempt_rebias是true所以不會執行
  if (mark->is_biased_anonymously() && !attempt_rebias) {
    markOop biased_value       = mark;
    markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
    markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
    if (res_mark == biased_value) {
      return BIAS_REVOKED;
    }
  //判斷物件是偏向模式
  } else if (mark->has_bias_pattern()) {
    Klass* k = obj->klass();
    markOop prototype_header = k->prototype_header();
    //判斷類不是偏向模式,即現在是處於批量撤銷延遲階段,需要修復物件的markword恢復成klass的markword非偏向模式
    if (!prototype_header->has_bias_pattern()) {
      //將類的markword cas替換到物件的markword,撤銷偏向鎖
      //邏輯還是cas替換,就不再詳細分析
      markOop biased_value       = mark;
      markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark);
      return BIAS_REVOKED;
    //如果類的epoch不等於物件的epoch,表明偏向已經過期
    } else if (prototype_header->bias_epoch() != mark->bias_epoch()) {
      //進行重偏向,通常再彙編程式碼中完成,但是到這裡是在執行期間,所以任何時刻可能會產生偏向過期
      //邏輯還是cas替換,就不再詳細分析
      if (attempt_rebias) {
        markOop biased_value       = mark;
        markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
        markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj->mark_addr(), mark);
        //成功則返回撤銷並重偏向
        if (res_mark == biased_value) {
          return BIAS_REVOKED_AND_REBIASED;
        }
      //因為attempt_rebias是true所以這個分支不會進入
      } else {
        markOop biased_value       = mark;
        markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
        markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
        if (res_mark == biased_value) {
          return BIAS_REVOKED;
        }
      }
    }
  }
  //這個方法會判斷是否需要批量撤銷和批量重偏向,這裡不進行展開了,有興趣的讀者可以自己展開分析
  //主要會返回幾個狀態
  //enum HeuristicsResult {
  //    HR_NOT_BIASED    = 1,  不需要偏向
  //    HR_SINGLE_REVOKE = 2,  單個撤銷
  //    HR_BULK_REBIAS   = 3,  批量重偏向
  //    HR_BULK_REVOKE   = 4   批量撤銷
  // };
  HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);
   //不需要偏向
  if (heuristics == HR_NOT_BIASED) {
    return NOT_BIASED;
  //單個撤銷
  } else if (heuristics == HR_SINGLE_REVOKE) {
    Klass *k = obj->klass();
    markOop prototype_header = k->prototype_header();
    //執行緒是當前執行緒,且沒有過期,直接撤銷
    if (mark->biased_locker() == THREAD &&
        prototype_header->bias_epoch() == mark->bias_epoch()) {
      ResourceMark rm;
      if (TraceBiasedLocking) {
        tty->print_cr("Revoking bias by walking my own stack:");
      }
      //撤銷方法
      BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD);
      ((JavaThread*) THREAD)->set_cached_monitor_info(NULL);
      return cond;
    } else {
      //說明是其他執行緒持有鎖,必須等到安全性點執行,宣告一個任務類
      VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
      VMThread::execute(&revoke);
      return revoke.status_code();
    }
  }
  
  //批量撤銷或重偏向任務類,根據 (heuristics == HR_BULK_REBIAS) 這個條件判斷是否是批量撤銷或者重偏向
  VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
                                (heuristics == HR_BULK_REBIAS),
                                attempt_rebias);
  VMThread::execute(&bulk_revoke);
  return bulk_revoke.status_code();
}

看到這裡大家可能對批量撤銷和重偏向操作有點疑惑,筆者先說明下關於批量撤銷和批量重定向的意義:

1.當一個執行緒建立了大量物件並執行了初始的同步操作,之後在另一個執行緒中將這些物件作為鎖進行之後的操作。這種case下,會導致大量的偏向鎖撤銷操作。

批量重偏向機制是為了解決這種場景

2.存在明顯多執行緒競爭的場景下還使用偏向鎖是不合適的,會產生大量的偏向鎖加鎖撤銷和升級

批量撤銷則是為了解決這第二種場景

批量操作實現的原理其實是在每個klass(class)中維護一個偏向鎖計數器,每一次該klass(class)的物件發生偏向撤銷操作時,該計數器+1,當這個值達到重偏向閾值(預設20,jvm引數BiasedLockingBulkRebiasThreshold控制)時,JVM就認為該class的偏向鎖有問題,因此會進行批量重偏向。

當達到重偏向閾值後,假設該kalss(class)計數器繼續增長,當其達到批量撤銷的閾值後(預設40,jvm引數BiasedLockingBulkRevokeThreshold控制,JVM就認為該klass(class)的使用場景存在多執行緒競爭,會先標記該klass(class)為非偏向模式即無鎖狀態,執行批量撤銷(在這裡就會產生批量撤銷延遲,即此時有些物件是偏向模式,但是其klass是非偏向模式,當其進行加鎖的時候就會通過一次cas修復makrword將其修復成非偏向模式直接走輕量級鎖邏輯),之後,對於該class的鎖,直接走輕量級鎖的邏輯。

關於批量操作的過程其實就是對單一操作的for迴圈,jvm會遍歷所有java執行緒,並遍歷執行緒中的棧幀獲取其中的lockRecord,判斷每個lockRecord中儲存的物件是否是當前klass(class)型別,如果是則進行撤銷。具體程式碼也比較簡單,筆者再這裡就不進行展開。

二.輕量級鎖競爭

我們繼續看輕量級鎖的競爭方法ObjectSynchronizer::slow_enter:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  //判斷是否是無鎖(中性)
  //到這裡無鎖狀態有兩種情況:
  //1.偏向鎖撤銷
  //2.曾經是輕量級鎖被釋放了
  if (mark->is_neutral()) {
    //替換lockrecord中的displaced_header為物件的markword
    lock->set_displaced_header(mark);
    //cas替換物件markword為lockRecord地址,成功則返回證明獲取輕量級鎖成功,失敗則進行鎖膨脹邏輯
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
  //判斷是否是輕量級鎖,且是否是當前執行緒持有,是則為重入
  } else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    //重入直接設定displaced_header為null並返回
    //表示新增一個Lock Record來表示鎖的重入
    lock->set_displaced_header(NULL);
    return;
  }
  //鎖膨脹邏輯
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

到這裡輕量級鎖的加鎖邏輯就結束了,熟悉輕量級鎖的朋友們可能會疑惑:

1.輕量級鎖競爭只有一次cas麼?就這樣結束了?傳說中的自旋等待呢?

這裡就是筆者所說的輕量級鎖騙局了,從原始碼中看輕量級鎖並不存在自旋邏輯,其競爭鎖的邏輯簡單到只有一次cas操作。

2.這樣操作豈不是就和偏向鎖一樣了,為什麼還要用輕量級鎖呢?

首先輕量級鎖設計之初是為了應對執行緒之間交替獲取鎖的場景,而偏向鎖的場景則是用於一個執行緒不斷獲取鎖的場景。通過原始碼我們可以看出當一個執行緒獲取偏向鎖後,這個鎖就會永久偏向這個執行緒,因為一旦發生偏向鎖撤銷,就代表鎖要升級成為輕量級鎖。雖然偏向鎖在加鎖時會進行一次cas操作,但是後續的獲取只會進行簡單的判斷,不會再進行cas操作。但是輕量級鎖的加鎖和釋放都需要進行cas操作。

我們看下如果把輕量級鎖使用在偏向鎖的場景下對比:

我們可以看到輕量級鎖情況下每次獲取都需要進行加鎖和釋放,每次加鎖和釋放都會進行cas操作,所以單個執行緒獲取鎖的情況使用偏向鎖效率更高。

在看下如果把偏向鎖使用在輕量級鎖的場景下對比:

除了第一次偏向鎖加鎖,以後每次偏向鎖加鎖時都要觸發偏向鎖的撤銷邏輯,通過剛剛的原始碼分析我們也可以看到如果執行緒B執行到偏向鎖程式碼塊時進行撤銷偏向鎖的行為因為鎖是執行緒A持有的,所以需要等到全域性安全點才可以進行撤銷操作,而且撤銷時也會進行一次cas操作。所以其效率肯定不如直接使用輕量級鎖,輕量級鎖儘管需要每次加鎖後都需要釋放,但是其在這種場景下不需要等到全域性安全點,在此場景下相對於偏向鎖更優。

3.輕量級鎖不使用自旋不會影響效率麼?

這個問題我們可以看輕量級鎖的設計場景,其是為了應對執行緒之間交替獲取鎖的場景。如果是執行緒交替獲取鎖的場景,就不會存在獲取鎖時cas競爭失敗,如果發生cas競爭失敗,則必然不是交替獲取鎖的場景,而是競爭更加激烈的場景,這個時候就需要鎖繼續膨脹升級了。所以輕量級鎖競爭沒有自旋的原因其實是其設計並不是用於處理過於激烈的競爭場景。

三.總結

通過對原始碼的學習,我們可以看到輕量級鎖和偏向鎖是用於處理不同場景的鎖,之前大家可能都認為輕量級鎖在競爭失敗後會自旋,然後多次嘗試再獲取鎖,其實通過本次原始碼學習,我們可以看到,並沒有自旋的邏輯。希望大家再看過本篇部落格後,可以正確的認識輕量級鎖,明白其設計意義和應對的場景。