1. 程式人生 > 其它 >JVM系列-synchronized詳解

JVM系列-synchronized詳解

1. MarkWord詳解

以上是Java物件處於5種不同狀態時,Mark Word中64個位的表現形式,上面每一行代表物件處於某種狀態時的樣子。其中各部分的含義如下:

  • lock:2位的鎖狀態標記位,由於希望用盡可能少的二進位制位表示儘可能多的資訊,所以設定了lock標記。該標記的值不同,整個Mark Word表示的含義不同。biased_lock和lock一起,表達的鎖狀態含義如下:
  • biased_lock:物件是否啟用偏向鎖標記,只佔1個二進位制位。為1時表示物件啟用偏向鎖,為0時表示物件沒有偏向鎖。lock和biased_lock共同表示物件處於什麼鎖狀態。

  • age:4位的Java物件年齡。在GC中,如果物件在Survivor區複製一次,年齡增加1。當物件達到設定的閾值時,將會晉升到老年代。預設情況下,並行GC的年齡閾值為15。由於age只有4位,所以最大值為15,這就是-XX:MaxTenuringThreshold選項最大值為15的原因。

  • identity_hashcode:31位的物件標識hashCode,採用延遲載入技術。呼叫方法System.identityHashCode()計算,並將該值儲存到Mark Word中。後續如果該物件的hashCode()方法再次被呼叫則不會再通過JVM進行計算得到,而是直接從Mark Word中獲取。只有這樣才能保證多次獲取到的identity hash code的值是相同的(以jdk8為例,JVM預設的計算identity hash code的方式得到的是一個隨機數,因而我們必須要保證一個物件的identity hash code只能被底層JVM計算一次),當一個物件已經計算過identity hash code,它就無法進入偏向鎖狀態,對於偏向鎖,線上程獲取偏向鎖時,會用Thread ID和epoch值覆蓋identity hash code所在的位置。如果一個物件的hashCode()方法已經被呼叫過一次之後,這個物件還能被設定偏向鎖麼?答案是不能。因為如果可以的化,那Mark Word中的identity hash code必然會被偏向執行緒Id給覆蓋,這就會造成同一個物件前後兩次呼叫hashCode()方法得到的結果不一致。當一個物件當前正處於偏向鎖狀態,並且需要計算其identity hash code的話,則它的偏向鎖會被撤銷,並且鎖會膨脹為輕量級鎖或者重量鎖;輕量級鎖的實現中,會通過執行緒棧幀的鎖記錄(Lock Record)儲存Displaced Mark Word;重量鎖的實現中,ObjectMonitor類裡有欄位可以記錄非加鎖狀態下的mark word,其中可以儲存identity hash code的值。

  • thread:持有偏向鎖的執行緒ID。

  • epoch:批量重偏向與批量撤銷。從偏向鎖的加鎖解鎖過程中可看出,當只有一個執行緒反覆進入同步塊時,偏向鎖帶來的效能開銷基本可以忽略,但是當有其他執行緒嘗試獲得鎖時,就需要等到safe point時,再將偏向鎖撤銷為無鎖狀態或升級為輕量級,會消耗一定的效能,所以在多執行緒競爭頻繁的情況下,偏向鎖不僅不能提高效能,還會導致效能下降。於是,就有了批量重偏向與批量撤銷的機制。原理以class為單位,為每個class維護批量重偏向(bulk rebias)機制是為了解決一個執行緒建立了大量物件並執行了初始的同步操作,後來另一個執行緒也來將這些物件作為鎖物件進行操作,這樣會導致大量的偏向鎖撤銷操作。批量撤銷(bulk revoke)機制是為了解決:在明顯多執行緒競爭劇烈的場景下使用偏向鎖是不合適的。一個偏向鎖撤銷計數器,每一次該class的物件發生偏向撤銷操作時,該計數器+1,當這個值達到重偏向閾值(預設20)時,JVM就認為該class的偏向鎖有問題,因此會進行批量重偏向。每個class物件會有一個對應的epoch欄位,每個處於偏向鎖狀態物件的Mark Word中也有該欄位,其初始值為建立該物件時class中的epoch的值。每次發生批量重偏向時,就將該值+1,同時遍歷JVM中所有執行緒的棧,找到該class所有正處於加鎖狀態的偏向鎖,將其epoch欄位改為新值。下次獲得鎖時,發現當前物件的epoch值和class的epoch不相等,那就算當前已經偏向了其他執行緒,也不會執行撤銷操作,而是直接通過CAS操作將其Mark Word的Thread Id 改成當前執行緒Id。當達到重偏向閾值後,假設該class計數器繼續增長,當其達到批量撤銷的閾值後(預設40),JVM就認為該class的使用場景存在多執行緒競爭,會標記該class為不可偏向,之後,對於該class的鎖,直接走輕量級鎖的邏輯。

  • ptr_to_lock_record:輕量級鎖狀態下,指向棧中鎖記錄的指標(Lock Record)。

  • ptr_to_heavyweight_monitor:重量級鎖狀態下,指向物件監視器Monitor的指標(ObjectMonitor)。

2. synchronized詳解

2.1 使用者態與核心態

JDK早期,synchronized 叫做重量級鎖, 因為申請鎖資源必須通過kernel, 系統呼叫(80中斷)

2.2 synchronized實現原理

2.2.1 java原始碼層級

synchronized(o)

2.2.2 位元組碼層級

同步方法:ACC_SYNCHRONIZED

同步程式碼塊:monitorenter、moniterexit

2.2.3 JVM層級(Hotspot)

synchronizer.cpp中InterpreterRuntime:: monitorenter方法

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);
  } 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");
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
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 {
      assert(!attempt_rebias, "can not rebias toward VM thread");
      BiasedLocking::revoke_at_safepoint(obj);
    }
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
 }

 slow_enter (obj, lock, THREAD) ;
}

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");

  if (mark->is_neutral()) {
    // Anticipate successful CAS -- the ST of the displaced mark must
    // be visible <= the ST performed by the CAS.
    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())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    lock->set_displaced_header(NULL);
    return;
  }

#if 0
  // The following optimization isn't particularly useful.
  if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
    lock->set_displaced_header (NULL) ;
    return ;
  }
#endif

  // The object header will never be displaced to this lock,
  // so it does not matter what the value is, except that it
  // must be non-zero to avoid looking like a re-entrant lock,
  // and must not look locked either.
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

inflate方法:膨脹為重量級鎖

2.2.3 OS和硬體層面

X86 : lock cmpxchg / xxx
https://blog.csdn.net/21aspnet/article/details/88571740

2.3 鎖升級過程

2.3.1 鎖型別

無鎖 - 偏向鎖 - 輕量級鎖(自旋鎖)- 重量級鎖

2.3.2 Markword和鎖升級

JVM一般是這樣使用鎖和Mark Word的:

  1. 當沒有被當成鎖時,這就是一個普通的物件,Mark Word記錄物件的HashCode,鎖標誌位是01,是否偏向鎖那一位是0。

  2. 當物件被當做同步鎖並有一個執行緒A搶到了鎖時,鎖標誌位還是01,但是否偏向鎖那一位改成1,前23bit記錄搶到鎖的執行緒id,表示進入偏向鎖狀態。【預設情況 偏向鎖有個時延,預設是4秒,因為JVM虛擬機器自己有一些預設啟動的執行緒,裡面有好多sync程式碼,這些sync程式碼啟動時就知道肯定會有競爭,如果使用偏向鎖,就會造成偏向鎖不斷的進行鎖撤銷和鎖升級的操作,效率較低。jvm偏向鎖延時引數 -XX:BiasedLockingStartupDelay】

  3. 當執行緒A再次試圖來獲得鎖時,JVM發現同步鎖物件的標誌位是01,是否偏向鎖是1,也就是偏向狀態,Mark Word中記錄的執行緒id就是執行緒A自己的id,表示執行緒A已經獲得了這個偏向鎖,可以執行同步鎖的程式碼。

  4. 當執行緒B試圖獲得這個鎖時,JVM發現同步鎖處於偏向狀態,但是Mark Word中的執行緒id記錄的不是B,那麼執行緒B會先用CAS操作試圖獲得鎖,這裡的獲得鎖操作是有可能成功的,因為執行緒A一般不會自動釋放偏向鎖。如果搶鎖成功,就把Mark Word裡的執行緒id改為執行緒B的id,代表執行緒B獲得了這個偏向鎖,可以執行同步鎖程式碼。如果搶鎖失敗,則繼續執行步驟5。

  5. 偏向鎖狀態搶鎖失敗,代表當前鎖有一定的競爭,偏向鎖將升級為輕量級鎖。JVM會在當前執行緒的執行緒棧中開闢一塊單獨的空間(LockRecord),裡面儲存指向物件鎖Mark Word的指標,同時在物件鎖Mark Word中儲存指向這片空間的指標。上述兩個儲存操作都是CAS操作,如果儲存成功,代表執行緒搶到了同步鎖,就把Mark Word中的鎖標誌位改成00,可以執行同步鎖程式碼。如果儲存失敗,表示搶鎖失敗,競爭太激烈,繼續執行步驟6。

  6. 輕量級鎖搶鎖失敗,JVM會使用自旋鎖,自旋鎖不是一個鎖狀態,只是代表不斷的重試,嘗試搶鎖。從JDK6開始,自旋鎖預設啟用,自旋次數由JVM決定。如果搶鎖成功則執行同步鎖程式碼,如果失敗則繼續執行步驟7。【競爭加劇:有執行緒超過10次自旋, -XX:PreBlockSpin, 或者自旋執行緒數超過CPU核數的一半, 1.6之後,加入自適應自旋 Adapative Self Spinning , JVM自己控制】【每個執行緒有自己的LockRecord在自己的執行緒棧上,用CAS去爭用markword的LR的指標,指標指向哪個執行緒的LR,哪個執行緒就擁有鎖,自旋超過10次,升級為重量級鎖 - 如果太多執行緒自旋 CPU消耗過大,不如升級為重量級鎖,進入等待佇列(不消耗CPU)-XX:PreBlockSpin。自旋鎖在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 來開啟。JDK 6 中變為預設開啟,並且引入了自適應的自旋鎖(自適應自旋鎖)。自適應自旋鎖意味著自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖物件上,自旋等待剛剛成功獲得過鎖,並且持有鎖的執行緒正在執行中,那麼虛擬機器就會認為這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞執行緒,避免浪費處理器資源。偏向鎖由於有鎖撤銷的過程revoke,會消耗系統資源,所以,在鎖爭用特別激烈的時候,用偏向鎖未必效率高。還不如直接使用輕量級鎖。】

  7. 自旋鎖重試之後如果搶鎖依然失敗,同步鎖會升級至重量級鎖,鎖標誌位改為10。在這個狀態下,未搶到鎖的執行緒都會被阻塞。【向作業系統申請資源,linux mutex , CPU從3級-0級系統呼叫,執行緒掛起,進入等待佇列,等待作業系統的排程,然後再映射回使用者空間,80中斷】

2.4 鎖重入

  • sychronized是可重入鎖
  • 重入次數必須記錄,因為要解鎖幾次必須得對應
  • 偏向鎖、自旋鎖重入次數是記錄線上程棧LockRecord中(重入多少次就有多少個LockRecord)
  • 重量級鎖重入次數是記錄ObjectMonitor類中欄位上

2.5 synchronized最底層實現

public class T {
    static volatile int i = 0;
    
    public static void n() { i++; }
    
    public static synchronized void m() {}
    
    publics static void main(String[] args) {
        for(int j=0; j<1000_000; j++) {
            m();
            n();
        }
    }
}

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly T

C1 Compile Level 1 (一級優化)

C2 Compile Level 2 (二級優化)

找到m() n()方法的彙編碼,會看到 lock comxchg .....指令

2.6 synchronized vs Lock (CAS)

  • 在高爭用 高耗時的環境下synchronized效率更高
  • 在低爭用 低耗時的環境下CAS效率更高
  • synchronized到重量級之後是等待佇列(不消耗CPU)
  • CAS(等待期間消耗CPU)

2.7 鎖消除 lock eliminate

public void add(String str1,String str2){
         StringBuffer sb = new StringBuffer();
         sb.append(str1).append(str2);
}

我們都知道 StringBuffer 是執行緒安全的,因為它的關鍵方法都是被 synchronized 修飾過的,但我們看上面這段程式碼,我們會發現,sb 這個引用只會在 add 方法中使用,不可能被其它執行緒引用(因為是區域性變數,棧私有),因此 sb 是不可能共享的資源,JVM 會自動消除 StringBuffer 物件內部的鎖。

2.8 鎖粗化 lock coarsening

public String test(String str){
       
       int i = 0;
       StringBuffer sb = new StringBuffer():
       while(i < 100){
           sb.append(str);
           i++;
       }
       return sb.toString():
}

JVM 會檢測到這樣一連串的操作都對同一個物件加鎖(while 迴圈內 100 次執行 append,沒有鎖粗化的就要進行 100 次加鎖/解鎖),此時 JVM 就會將加鎖的範圍粗化到這一連串的操作的外部(比如 while 虛幻體外),使得這一連串操作只需要加一次鎖即可。

2.9 鎖降級

https://www.zhihu.com/question/63859501

其實,只被VMThread訪問,降級也就沒啥意義了。所以可以簡單認為鎖降級不存在!

3. 思考

  1. 為什麼有自旋鎖還需要重量級鎖?

    自旋是消耗CPU資源的,如果鎖的時間長,或者自旋執行緒多,CPU會被大量消耗。重量級鎖有等待佇列,所有拿不到鎖的進入等待佇列,不需要消耗CPU資源

  2. 偏向鎖是否一定比自旋鎖效率高?

    不一定,在明確知道會有多執行緒競爭的情況下,偏向鎖肯定會涉及鎖撤銷,這時候直接使用自旋鎖。JVM啟動過程,會有很多執行緒競爭(明確),所以預設情況啟動時不開啟偏向鎖,過一段兒時間再開啟