synchronized實現原理及鎖優化

weixin_33716557發表於2018-05-14

1.引言

併發程式設計中synchronized是重量級鎖,但隨著JVM1.6對synchronized進行優化後,有些情況下它並不那麼重,本文介紹了Java SE1.6中為了減少獲得鎖和釋放鎖帶來的效能消耗而引入的偏向鎖和輕量級鎖,以及鎖的儲存結構和升級過程。

2.術語定義

CAS(Compare and Swap): 比較並交換。用於在硬體層面上提供原子性操作。在 Intel 處理器中,比較並交換通過指令cmpxchg實現。比較是否和給定的數值一致,如果一致則修改,不一致則不修改。

3.同步的基礎

Java中的每一個物件都可以作為鎖。
對於同步方法,鎖是當前例項物件。
對於靜態同步方法,鎖是當前物件的Class物件。
對於同步方法塊,鎖是synchonized括號裡配置的物件。

我們通過java程式碼和位元組碼分析下
java程式碼如下:

public class SyncTest {

    private static double a = 1;

    public synchronized void plusNumber() {
        a++;
    }

    public void minusNumber() {
        System.out.println(a);
        synchronized (this) {
            a--;
        }
    }

    public synchronized static void divide() {

        a = a / 0.1;
    }

}

解析成位元組碼指令:

//同步方法
public synchronized void plusNumber();                                                                       
  descriptor: ()V                                                                                            
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED                                                                        
  Code:                                                                                                      
    stack=4, locals=1, args_size=1                                                                           
       0: getstatic     #2                  // Field a:D                                                     
       3: dconst_1                                                                                           
       4: dadd                                                                                               
       5: putstatic     #2                  // Field a:D                                                     
       8: return                                                                                             
    LineNumberTable:                                                                                         
      line 12: 0                                                                                             
      line 13: 8                                                                                             

//同步塊                                                                                                             
public void minusNumber();                                                                                   
  descriptor: ()V                                                                                            
  flags: ACC_PUBLIC                                                                                          
  Code:                                                                                                      
    stack=4, locals=3, args_size=1                                                                           
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;              
       3: getstatic     #2                  // Field a:D                                                     
       6: invokevirtual #4                  // Method java/io/PrintStream.println:(D)V                       
       9: aload_0                                                                                            
      10: dup                                                                                                
      11: astore_1                                                                                           
      12: monitorenter                                                                                       
      13: getstatic     #2                  // Field a:D                                                     
      16: dconst_1                                                                                           
      17: dsub                                                                                               
      18: putstatic     #2                  // Field a:D                                                     
      21: aload_1                                                                                            
      22: monitorexit                                                                                        
      23: goto          31                                                                                   
      26: astore_2                                                                                           
      27: aload_1                                                                                            
      28: monitorexit                                                                                        
      29: aload_2                                                                                            
      30: athrow                                                                                             
      31: return                                                                                             
    Exception table:                                                                                         
       from    to  target type                                                                               
          13    23    26   any                                                                               
          26    29    26   any                                                                               
                                                                   
//靜態同步方法                                                                                                             
public static synchronized void divide();                                                                    
  descriptor: ()V                                                                                            
  flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED                                                            
  Code:                                                                                                      
    stack=4, locals=0, args_size=0                                                                           
       0: getstatic     #2                  // Field a:D                                                     
       3: ldc2_w        #5                  // double 0.1d                                                   
       6: ddiv                                                                                               
       7: putstatic     #2                  // Field a:D                                                     
      10: return                                                                                             
    LineNumberTable:                                                                                         
      line 24: 0                                                                                             
      line 26: 10                                                                                            
                                                                                                             

從上述指令我們可以得出以下結論:

  1. 同步程式碼塊是使用monitorenter和monitorexit指令實現的,會在同步塊的區域通過監聽器物件去獲取鎖和釋放鎖,從而在位元組碼層面來控制同步scope.
  2. 同步方法和靜態同步方法依靠的是方法修飾符上的ACC_SYNCHRONIZED實現。JVM根據該修飾符來實現方法的同步。當方法呼叫時,呼叫指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,如果設定了,執行執行緒將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其他任何執行緒都無法再獲得同一個monitor物件

當一個執行緒訪問同步程式碼塊時,根據happens-before原則,它必須獲取鎖才能進入程式碼塊,退出或丟擲異常時必須釋放鎖。那麼鎖存在哪裡?鎖裡面會儲存什麼資訊?

4. 同步的原理

4.1 Java物件頭
HotSpot虛擬機器中,物件在記憶體中儲存分為三塊區域:物件頭、例項資料和對齊填充
HotSpot虛擬機器的物件頭(Object Header)包括兩部分資訊:
第一部分"Mark Word": 用於儲存物件自身的執行時資料, 如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等等.

第二部分"Klass Pointer": 物件指向它的類的後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。(陣列,物件頭中還須有一塊用於記錄陣列長度的資料,因為虛擬機器可以通過普通Java物件的後設資料資訊確定Java物件的大小,但是從陣列的後設資料中無法確定陣列的大小。 )

物件頭預設儲存結構

10175660-8e2e456b957e939c.JPG
預設儲存結構

在執行期間,Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化。Mark Word可能變化為儲存以下4種資料(32位系統)

10175660-90c674b807003850.JPG
執行時儲存結構變化

物件頭的資料結構C++原始碼可以檢視 openjdk\hotspot\src\share\vm\oops.markOop.hpp

// 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)
//
hash: 儲存物件的雜湊碼
age: 儲存物件的分代年齡
biased_lock: 偏向鎖標識位
lock: 鎖狀態標識位
JavaThread*: 儲存持有偏向鎖的執行緒ID
epoch: 儲存偏向時間戳

4.2 Monitor Record

監聽器的實現依賴於objectMonitor物件原始碼如下:
openjdk\hotspot\src\share\vm\runtime\objectMonitor.hpp

監聽器具體儲存的資料結構

ObjectMonitor() {
    _header       = NULL;//markOop物件頭
    _count        = 0;
    _waiters      = 0,//等待執行緒數
    _recursions   = 0;//重入次數
    _object       = NULL;//監視器鎖寄生的物件。鎖不是平白出現的,而是寄託儲存於物件中。
    _owner        = NULL;//初始時為NULL表示當前沒有任何執行緒擁有該monitor record,當執行緒成功擁有該鎖後儲存執行緒唯一標識,當鎖被釋放時又設定為NULL
    _WaitSet      = NULL;//處於wait狀態的執行緒,會被加入到wait set;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;//處於等待鎖block狀態的執行緒,會被加入到entry set;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
  }

每個執行緒都有兩個ObjectMonitor物件列表,分別為free和used列表,如果當前free列表為空,執行緒將向全域性global list請求分配ObjectMonitor。
ObjectMonitor物件中有兩個佇列:_WaitSet 和 _EntryList,用來儲存ObjectWaiter物件列表;


10175660-d31da0ecab6cd54c.png
monitor

5. JVM中鎖的優化

5.1 鎖機制升級流程
偏向鎖--》輕量級鎖--》重量級鎖

synchronized在JVM被編譯為monitorenter、monitorexit指令來獲取和釋放互斥鎖.。
直譯器執行monitorenter時會進入到openjdk\hotspot\src\share\vm\interpreter\InterpreterRuntime.cpp的InterpreterRuntime::monitorenter函式,
具體實現如下:

//%note monitor_1
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) {//標識虛擬機器是否開啟偏向鎖功能,預設開啟
    //偏向鎖邏輯
    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

5.2 偏向鎖
大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID,以後該執行緒在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下物件頭的Mark Word裡是否儲存著指向當前執行緒的偏向鎖。如果測試成功,表示執行緒已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖):如果沒有設定,則使用CAS競爭鎖;如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒。
注意:當鎖有競爭關係的時候,需要解除偏向鎖,進入輕量級鎖。
偏向鎖執行流程,執行緒1演示了偏向鎖初始化的流程,執行緒2演示了偏向鎖撤銷的流程

10175660-2b9a1f8e0028dd6e.jpg
偏向鎖

偏向鎖在Java 6和Java 7裡是預設啟用的,但是它在應用程式啟動幾秒鐘之後才啟用,如有必要可以使用JVM引數來關閉延遲:XX:BiasedLockingStartupDelay=0。如果確定應用的鎖通常情況下處於競爭狀態,可以通過JVM引數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程式預設會進入輕量級鎖狀態。

偏向鎖獲取的具體邏輯在openjdk\hotspot\src\share\vm\runtime\synchronizer.cpp的ObjectSynchronizer::fast_enter函式下,具體程式碼如下:

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 if (UseBiasedLocking) {//jvm是否開啟了偏向鎖
    //是否在全域性安全點,如達到安全點撤銷偏向鎖,同時升級為輕量級鎖
    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) ;
}

5.3 輕量級鎖
1>加鎖
執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。然後執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標。如果成功,當前執行緒獲得鎖,如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖。
2>解鎖
輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到物件頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。
下圖展示兩個執行緒同時爭奪鎖,導致鎖膨脹的流程圖:

10175660-94e49823fb3588d7.jpg
輕量級鎖

自旋的執行緒在自旋過程中,成功獲得資源(即之前獲的資源的執行緒執行完成並釋放了共享資源),則整個狀態依然處於輕量級鎖的狀態,如果自旋失敗進入重量級鎖的狀態,這個時候,自旋的執行緒進行阻塞,等待之前執行緒執行完成並喚醒自己。因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的執行緒被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他執行緒試圖獲取鎖時,都會被阻塞住,當持有鎖的執行緒釋放鎖之後會喚醒這些執行緒,被喚醒的執行緒就會進行新一輪的奪鎖之爭。

多個執行緒競爭偏向鎖升級為輕量級鎖,會嘗試獲取輕量級鎖,其入口位於ObjectSynchronizer::slow_enter函式,具體程式碼如下:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();//獲取物件頭的Mark Word
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");

  if (mark->is_neutral()) {//是否為無鎖狀態001
    // Anticipate successful CAS -- the ST of the displaced mark must
    // be visible <= the ST performed by the CAS.
    lock->set_displaced_header(mark);//把mark儲存到BasicLock物件的_displaced_header欄位
    //原子操作保證只有一個執行緒可以把指向棧幀的指標複製到Mark Word
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {//CAS成功,釋放棧鎖
      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);
}

5.4 鎖的優缺點對比

10175660-0c7782fa4c2024c3.JPG
鎖對比

5.5 鎖粗化:就是將多次連線在一起的加鎖、解鎖操作合併為一次,將多個連續的鎖擴充套件成一個範圍更大的鎖。舉個例子:

public class LockCoarsening {

    private StringBuffer stringBuffer = new StringBuffer(20);

    public void append(){
        stringBuffer.append("w");
        stringBuffer.append("h");
        stringBuffer.append("y");
    }

}

這裡每次呼叫stringBuffer.append方法都需要加鎖和解鎖,如果虛擬機器檢測到有一系列連串的對同一個物件加鎖和解鎖操作,就會將其合併成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。

5.6 鎖消除
鎖消除即刪除不必要的加鎖操作。根據程式碼逃逸技術,如果判斷到一段程式碼中,堆上的資料不會逃逸出當前執行緒,那麼可以認為這段程式碼是執行緒安全的,不必要加鎖。逃逸分析和鎖消除分別可以使用引數-XX:+DoEscapeAnalysis和-XX:+EliminateLocks(鎖消除必須在-server模式下)開啟

5.7 適應性自旋
當前鎖處於膨脹,會進行自旋。自旋是需要消耗CPU的,如果一直獲取不到鎖的話,那執行緒一直處在自旋狀態,消耗CPU資源。為了解決這個問題JDK採用—適應性自旋,執行緒如果自旋成功了,則下次自旋的次數會更多,如果自旋失敗了,則自旋的次數就會減少。另外自旋雖然會佔用CPU資源,但不會一直佔用CPU資源,每隔一段時間會通過os::NakedYield方法放棄CPU資源,或通過park方法掛起;如果其他執行緒完成鎖的膨脹操作,則退出自旋並返回

Reference

併發程式設計網-Java SE1.6中的synchronized
JVM原始碼分析之synchronized實現
Implementing Fast Java Monitors with Relaxed-Locks

相關文章