監視器鎖 synchronized


 

一、synchronized 的原理
JVM 基於進入和推出Monitor對象來實現方法和同步代碼塊,但兩者的實現細節不同。
  • synchronize 修飾的同步代碼塊:使用monitorenter 和 monitorexit 指令實現;
  • synchronize 修飾的方法並沒有 monitorenter 和 monitorexit 指令 ,而取代之的是 ACC_SYNCHRONIZED標識,該標志指明了該方法是一個同步方法,從而執行相應的同步調用。
 
Monitorenter 指令實現編譯后到同步代碼塊開始的位置,而 monitorexit 是插入在方法結束出和異常處,JVM要保證每個monitorter 必須要有一個與之對應的monitorexit 。任何一個對象都可以和一個monitor 相關聯,且當一個執行 monitorenter 指令的時候,會嘗試獲取synchronized 圓括號中對象中相關聯的 monitor的所有權,即嘗試獲取這個對象的鎖(這里的是重量級鎖,為沒有后面提到的偏向鎖和輕量級鎖)

 

(1)synchronized修飾的方法

public synchronized void method() {
    int i = 0;
}

javap反編譯后:

  public synchronized void method();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: return
      LineNumberTable:
        line 6: 0
        line 7: 2
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Lcom/lock/SyncTest;
            2       1     1     i   I

對於同步方法,JVM會講方法設置 ACC_SYNCHRONIZED 標志,調用的時候 JVM 根據這個標志判斷是否是同步方法。

 

(2)synchronized修改的代碼塊

public void method() {
    synchronized (this) {
        int i = 0;
    }
}

javap反編譯后:

  public void method();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: iconst_0
         5: istore_2
         6: aload_1
         7: monitorexit
         8: goto          14
        11: aload_1
        12: monitorexit
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     8    11   any
            11    13    11   any
      LineNumberTable:
        line 6: 0
        line 7: 4
        line 6: 6
        line 9: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lcom/lock/SyncTest;
對於Synchronized的同步代碼塊,JVM會在進入代碼塊之前加上monitorenter ,如果進入monitor成功,線程便獲取了鎖,一個對象的monitor同一時刻只能被一個線程鎖占有;
上面為什么有兩個monitorexit呢?第一個是正常退出,第二個是異常退出,保證不會死鎖。
 
monitorenter和 monitorexit源碼剖析
openjdk\hotspot\src\share\vm\interpreter\ interpreterRuntime.cpp
// Synchronization
//
// The interpreter's synchronization code is factored out so that it can
// be shared by method invocation and synchronized blocks.
//%note synchronization_3

//%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");
  // 是否開啟偏向鎖功能,JVM參數:+UseBiasedLocking表示開啟偏向鎖
  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

 

// ---------------------------------偏向鎖的獲取--------------------------------------------
//  Fast Monitor Enter/Exit
// This the fast monitor enter. The interpreter and compiler use
// some assembly copies of this code. Make sure update those code
// if the following function is changed. The implementation is
// extremely sensitive to race condition. Be careful.

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);
    }
    // 對象頭是否有偏向標記,如果沒有提示偏向鎖將被取消,執行slow_enter
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
 }
 // 
 slow_enter (obj, lock, THREAD) ;
}


// --------------------------------輕量級鎖的獲取---------------------------------------------
// Interpreter/Compiler Slow Case
// This routine is used to handle interpreter/compiler slow case
// We don't need to use fast path here, because it must have been
// failed in the interpreter/compiler code.
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);
    // CAS加鎖操作,如果成功則結束,否則繼續執行后續代碼...
    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");
    // 鎖指向null
    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);
}

 

 
 
 
 
二、Monitor監視器鎖
其中輕量級鎖和偏向鎖是Java6對synchronized鎖進行優化后增加的,我們稍后會進行分析。這里我們主要分析重量級鎖,也就是通常所說的synchronized對象鎖,鎖標識為10,其中指針指向monitor對象(也稱之為管程或者監視器鎖)的起始地址。每個對象都存在一個monitor與之關聯,對象與其monitor之間也存在着多種實現方式,如monitor可以與對象一起創建或者銷毀或當前線程試圖獲取鎖時自動生成,但一個monitor被某線程持有后,它便處於鎖定狀態。在Java虛擬機(HotSpot)中,monitor是有ObjectMonitor實現的,其主要數據結構如下(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的)
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;        // 記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;    // 記錄當前持有鎖的線程ID
    _WaitSet      = NULL;    // 等待池:處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;    // 鎖池:處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

ObjectMonitor 中有兩個隊列,_WaitSet 和 _EntryList ,用來保存ObjectWaiter 對象列表,每個等待鎖的線程都會被封裝成ObjectWaiter 對象,_owner 指向持有ObjectMonitor 對象的線程,當多個線程同時訪問同一同步代碼塊或者同步方法時,首先會進入 _EntryList 隊列,當線程獲取到monitor 后進入_Owner 區域並把 monitor中的 _Owner 變量設置為當前線程,同時monitor 中的計數器count 加1,若線程調用wait() 方法,將釋放當前持有的monitor,_owner變量恢復為null,count 自減 1 ,同時該線程進入_WaitSet 集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。如下圖所示:

每個對象都有一個Monitor對象相關聯,Monitor對象中記錄了持有鎖的線程信息、等待隊列等。Monitor對象包含以下三個字段:
  • _owner 記錄當前持有鎖的線程ID
  • _EntryList 是一個隊列,記錄所有阻塞等待鎖的線程(阻塞隊列,鎖池)
  • _WaitSet 也是一個隊列,記錄調用 wait() 方法並還未被通知的線程(等待池)
當線程持有鎖的時候,線程id等信息會拷貝進owner字段,其余線程會進入阻塞隊列entrylist,當持有鎖的線程執行wait方法,會立即釋放鎖進入waitset,當線程釋放鎖的時候,owner會被置空,公平鎖條件下,entrylist中的線程會競爭鎖,競爭成功的線程id會寫入owner,其余線程繼續在entrylist中等待。
 
采用Synchronized給對象加鎖會使線程阻塞,因而會造成線程狀態的切換,而線程狀態的切換必須要操作系統來執行,因此需要將用戶態切換為內核態,這個切換的過程是十分耗時的都需要操作系統來幫忙,有可能比用戶執行代碼的時間還要長。
 
所以,monitor對象存在於每一個java對象的對象頭(存儲指針的指向),synchronized鎖便是通過這種方式獲取的,也是為什么java中任何對象都可以作為鎖的原因,同時也是 notify/notifyAll/wait 方法等存在於頂級對象Object中的原因。
 
Wait/Notify/NotifyAll 源碼
openjdk\hotspot\src\share\vm\runtime\ s ynchronizer.cpp
// -----------------------------------------------------------------------------
//  Wait/Notify/NotifyAll
// NOTE: must use heavy weight monitor to handle wait()
void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) {
  // 是否開啟偏向鎖
  if (UseBiasedLocking) {
    // 撤銷偏向鎖
    BiasedLocking::revoke_and_rebias(obj, false, THREAD);
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
  }
  if (millis < 0) {
    TEVENT (wait - throw IAX) ;
    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
  }
  // 鎖膨脹(升級為重量級鎖)
  ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj());
  DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis);
  // 調用ObjectMonitor的wait方法
  monitor->wait(millis, true, THREAD);

  /* This dummy call is in place to get around dtrace bug 6254741.  Once
     that's fixed we can uncomment the following line and remove the call */
  // DTRACE_MONITOR_PROBE(waited, monitor, obj(), THREAD);
  dtrace_waited_probe(monitor, obj, THREAD);
}


void ObjectSynchronizer::notify(Handle obj, TRAPS) {
 if (UseBiasedLocking) {
    // 撤銷偏向鎖
    BiasedLocking::revoke_and_rebias(obj, false, THREAD);
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
  }

  markOop mark = obj->mark();
  // 如果是輕量級鎖,並且當前線程擁有該鎖,直接返回
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    return;
  }
  // 鎖膨脹(升級為重量級鎖),並調用ObjectMonitor的notify方法
  ObjectSynchronizer::inflate(THREAD, obj())->notify(THREAD);
}

// NOTE: see comment of notify()
void ObjectSynchronizer::notifyall(Handle obj, TRAPS) {
  if (UseBiasedLocking) {
    // 撤銷偏向鎖
    BiasedLocking::revoke_and_rebias(obj, false, THREAD);
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
  }

  markOop mark = obj->mark();
  // 如果是輕量級鎖,並且當前線程擁有該鎖,直接返回
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    return;
  }
  // 鎖膨脹(升級為重量級鎖),並調用ObjectMonitor的notifyAll方法
  ObjectSynchronizer::inflate(THREAD, obj())->notifyAll(THREAD);
}

 

 

 
 
三、synchronized的可重入性
從互斥鎖的設計上來說,當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,將會處於阻塞狀態,但當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬於重入鎖,也叫遞歸鎖,請求將會成功, 在java中synchronized是基於原子性的內部鎖機制,是可重入的,因此在一個線程調用synchronized方法的同時在其方法體內部調用該對象另一個synchronized方法,也就是說一個線程得到一個對象鎖后再次請求該對象鎖,是允許的,這就是synchronized的可重入性。如下:
public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){

            //this,當前實例對象鎖
            synchronized(this){
                i++;
                increase();//synchronized的可重入性
            }
        }
    }

    public synchronized void increase(){
        j++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

正如代碼所演示的,在獲取當前實例對象鎖后進入synchronized代碼塊執行同步代碼,並在代碼塊中調用了當前實例對象的另外一個synchronized方法,再次請求當前實例鎖時,將被允許,進而執行方法體代碼,這就是重入鎖最直接的體現,需要特別注意另外一種情況,當子類繼承父類時,子類也是可以通過可重入鎖調用父類的同步方法。注意由於synchronized是基於monitor實現的,因此每次重入,monitor中的計數器仍會加1。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM