Java synchronized 关键字(3)-JVM 重量级锁 Monitor 的实现

1. Java 中的 Monitor

Java synchronized 关键字(1)-使用全解 中介绍过synchronize的实现原理,其实无论是同步方法ACC_SYNCHRONIZED还是同步代码块 monitorentermonitorexit都是基于锁对象的对象头实现的。而 Monitor 位于 Java 对象的对象头里面,是 Java 中用以实现线程之间的互斥与协作的重要手段,它可以看成是对象的锁,每一个对象都有且仅有一个 Monitor它的职责是保证同一时间只有一个线程能够访问被保护的数据和代码,其发生作用时的特点是:

  1. 对象的目标代码都被互斥地执行。一个 Monitor 只允许一个线程持有,任一个线程访问被保护的代码都需要获得这个“许可”,执行完毕时释放所有权
  2. 提供信号机制:允许正持有 Monitor 的线程暂时放弃持有,等待某种条件成立后,进程可以通知正在等待这个条件变量的线程,使其可以重新去尝试持有 Monitor

需注意 Monitor 为 JVM 重量级锁的实现机制,JVM 对锁的优化有偏向锁,轻量级锁等,这部分可参考 Java synchronized 关键字(2)-JVM 的锁实现分析

2. Monitor 实现锁分配

2.1 Monitor 的结构

在 Java 虚拟机 (HotSpot)中,Monitor 基于C++实现,其数据结构为 objectMonitor.hpp ,比较关键的属性如下:

 ObjectMonitor() {
    ......
    // 用来记录该线程获取锁的次数
    _count        = 0;
     // 锁的重入次数
    _recursions   = 0;
    // 指向持有 ObjectMonitor 锁的对象的引用
    _owner        = NULL;
    // 存放处于wait状态的线程队列
    _WaitSet      = NULL;
    // 存放处于等待锁block状态的线程队列
    _EntryList    = NULL ;
    ......
  }

锁相关主要的流程如下:

  1. 当多个线程同时访问一段同步代码时,首先进入_EntryList队列中,当某个线程获取到对象的Monitor后进入_Owner区域并把Monitor中的_owner变量设置为当前线程,同时Monitor中的计数器_count自增1,表示获得对象锁
  2. 若持有Monitor的线程调用wait()方法,将释放当前持有的 Monitor_owner变量恢复为 null,_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也会释放Monitor(锁)并复位变量的值,以便其他线程能够获取Monitor(锁)
  3. Entry Set中等待的线程状态是 Waiting for monitor entry,而在 Wait Set中等待的线程状态是 in Object.wait()

在这里插入图片描述

2.2 获取锁流程

在这里插入图片描述

objectMonitor.cpp 为实现锁相关操作的关键类,其中 enter() 函数为重量级锁获取的入口。这个函数的逻辑比较散乱,大致可以分为以下几步:

  1. 首先 CAS 尝试把 monitor 的_owner字段设置为当前请求锁的线程,该操作进行时期望的_owner字段值为 NULL。根据 CAS 返回的值进行相关判断,此处有 3 种情况:
    1. 如果 CAS 返回了 NULL,则表明 CAS 操作失败,直接返回
    2. 如果 CAS 返回的 monitor 的_owner字段旧值与当前请求锁的线程一致,则说明是锁重入,更新计数器即可
    3. 调用线程的 Thread::is_lock_owned() 方法判断请求锁的线程是否获取到了锁,如果是的话更新 monitor 相关属性返回
  2. CAS 获取锁失败了,调用 TrySpin() 方法尝试自旋获取锁,成功获取到锁则返回,未成功继续以下流程
  3. 修改当前请求锁的 Java 线程的状态为阻塞在 monitor enter,for 空循环执行 ObjectMonitor::EnterI()函数去获取锁
void ATTR ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD ;
 void * cur ;
 // CAS尝试把monitor的`_owner`字段设置为当前线程
 cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
 // 获取锁失败
 if (cur == NULL) {
    // Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
    assert (_recursions == 0   , "invariant") ;
    assert (_owner      == Self, "invariant") ;
    // CONSIDER: set or assert OwnerIsThread == 1
    return ;
 }
 // 旧值和当前线程一样,则当前线程已经持有锁,此次为重入,_recursions自增,并获得锁
 if (cur == Self) {
    // TODO-FIXME: check for integer overflow!  BUGID 6557169.
    _recursions ++ ;
    return ;
 }

 // 当前线程是第一次进入该 monitor,设置_recursions为1,_owner为当前线程
 if (Self->is_lock_owned ((address)cur)) {
   assert (_recursions == 0, "internal state error");
   _recursions = 1 ;
   // Commute owner from a thread-specific on-stack BasicLockObject address to
   // a full-fledged "Thread *".
   _owner = Self ;
   OwnerIsThread = 1 ;
   return ;
 }

 // We've encountered genuine contention.
 assert (Self->_Stalled == 0, "invariant") ;
 Self->_Stalled = intptr_t(this) ;

 // Try one round of spinning *before* enqueueing Self
 // and before going through the awkward and expensive state
 // transitions.  The following spin is strictly optional ...
 // Note that if we acquire the monitor from an initial spin
 // we forgo posting JVMTI events and firing DTRACE probes.
 if (Knob_SpinEarly && TrySpin (Self) > 0) {
    assert (_owner == Self      , "invariant") ;
    assert (_recursions == 0    , "invariant") ;
    assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
    Self->_Stalled = 0 ;
    return ;
 }

 assert (_owner != Self          , "invariant") ;
 assert (_succ  != Self          , "invariant") ;
 assert (Self->is_Java_thread()  , "invariant") ;
 JavaThread * jt = (JavaThread *) Self ;
 assert (!SafepointSynchronize::is_at_safepoint(), "invariant") ;
 assert (jt->thread_state() != _thread_blocked   , "invariant") ;
 assert (this->object() != NULL  , "invariant") ;
 assert (_count >= 0, "invariant") ;

 // Prevent deflation at STW-time.  See deflate_idle_monitors() and is_busy().
 // Ensure the object-monitor relationship remains stable while there's contention.
 Atomic::inc_ptr(&_count);

 EventJavaMonitorEnter event;

 { // Change java thread status to indicate blocked on monitor enter.
   JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);

   Self->set_current_pending_monitor(this);

   DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
   if (JvmtiExport::should_post_monitor_contended_enter()) {
     JvmtiExport::post_monitor_contended_enter(jt, this);

     // The current thread does not yet own the monitor and does not
     // yet appear on any queues that would get it made the successor.
     // This means that the JVMTI_EVENT_MONITOR_CONTENDED_ENTER event
     // handler cannot accidentally consume an unpark() meant for the
     // ParkEvent associated with this ObjectMonitor.
   }

   OSThreadContendState osts(Self->osthread());
   ThreadBlockInVM tbivm(jt);

   // 空循环执行 ObjectMonitor::EnterI 方法等待锁的释放
   for (;;) {
     jt->set_suspend_equivalent();
     // cleared by handle_special_suspend_equivalent_condition()
     // or java_suspend_self()

     EnterI (THREAD) ;

     if (!ExitSuspendEquivalent(jt)) break ;

     //
     // We have acquired the contended monitor, but while we were
     // waiting another thread suspended us. We don't want to enter
     // the monitor while suspended because that would surprise the
     // thread that suspended us.
     //
         _recursions = 0 ;
     _succ = NULL ;
     exit (false, Self) ;

     jt->java_suspend_self();
   }
   Self->set_current_pending_monitor(NULL);

   // We cleared the pending monitor info since we've just gotten past
   // the enter-check-for-suspend dance and we now own the monitor free
   // and clear, i.e., it is no longer pending. The ThreadBlockInVM
   // destructor can go to a safepoint at the end of this block. If we
   // do a thread dump during that safepoint, then this thread will show
   // as having "-locked" the monitor, but the OS and java.lang.Thread
   // states will still report that the thread is blocked trying to
   // acquire it.
 }
 
  ......
  
 OM_PERFDATA_OP(ContendedLockAttempts, inc());
}

ObjectMonitor::EnterI()函数的主要逻辑是处理锁的竞争,不过大致分为了以下几步:

  1. 首先依然是挂起线程前的TrySpin()自旋尝试,如果获取到了锁 return 返回即可
  2. 将请求锁的线程封装为 ObjectWaiter 对象,插入到 _cxq列表头。_cxq列表中保存的 ObjectWaiter 对象将在持有锁的线程释放锁时按照一定的规则移动到 _EntryList,等待唤醒
  3. for 空循环中请求锁的线程如果没有获取到锁会自行挂起,被唤醒后将从挂起的地方继续执行,继续尝试获取锁。一旦获取到锁则跳出锁竞争的空循环,并调用 UnlinkAfterAcquire() 函数将自身从等待锁的队列中移除
void ATTR ObjectMonitor::EnterI (TRAPS) {
    Thread * Self = THREAD ;
    assert (Self->is_Java_thread(), "invariant") ;
    assert (((JavaThread *) Self)->thread_state() == _thread_blocked   , "invariant") ;

    // Try the lock - TATAS
    if (TryLock (Self) > 0) {
        assert (_succ != Self              , "invariant") ;
        assert (_owner == Self             , "invariant") ;
        assert (_Responsible != Self       , "invariant") ;
        return ;
    }

    DeferredInitialize () ;

    // We try one round of spinning *before* enqueueing Self.
    //
    // If the _owner is ready but OFFPROC we could use a YieldTo()
    // operation to donate the remainder of this thread's quantum
    // to the owner.  This has subtle but beneficial affinity
    // effects.

    if (TrySpin (Self) > 0) {
        assert (_owner == Self        , "invariant") ;
        assert (_succ != Self         , "invariant") ;
        assert (_Responsible != Self  , "invariant") ;
        return ;
    }

    // The Spin failed -- Enqueue and park the thread ...
    assert (_succ  != Self            , "invariant") ;
    assert (_owner != Self            , "invariant") ;
    assert (_Responsible != Self      , "invariant") ;

    // Enqueue "Self" on ObjectMonitor's _cxq.
    //
    // Node acts as a proxy for Self.
    // As an aside, if were to ever rewrite the synchronization code mostly
    // in Java, WaitNodes, ObjectMonitors, and Events would become 1st-class
    // Java objects.  This would avoid awkward lifecycle and liveness issues,
    // as well as eliminate a subset of ABA issues.
    // TODO: eliminate ObjectWaiter and enqueue either Threads or Events.
    //

    ObjectWaiter node(Self) ;
    Self->_ParkEvent->reset() ;
    node._prev   = (ObjectWaiter *) 0xBAD ;
    node.TState  = ObjectWaiter::TS_CXQ ;

    // Push "Self" onto the front of the _cxq.
    // Once on cxq/EntryList, Self stays on-queue until it acquires the lock.
    // Note that spinning tends to reduce the rate at which threads
    // enqueue and dequeue on EntryList|cxq.
    ObjectWaiter * nxt ;
    for (;;) {
        node._next = nxt = _cxq ;
        if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;

        // Interference - the CAS failed because _cxq changed.  Just retry.
        // As an optional optimization we retry the lock.
        if (TryLock (Self) > 0) {
            assert (_succ != Self         , "invariant") ;
            assert (_owner == Self        , "invariant") ;
            assert (_Responsible != Self  , "invariant") ;
            return ;
        }
    }

    // Check for cxq|EntryList edge transition to non-null.  This indicates
    // the onset of contention.  While contention persists exiting threads
    // will use a ST:MEMBAR:LD 1-1 exit protocol.  When contention abates exit
    // operations revert to the faster 1-0 mode.  This enter operation may interleave
    // (race) a concurrent 1-0 exit operation, resulting in stranding, so we
    // arrange for one of the contending thread to use a timed park() operations
    // to detect and recover from the race.  (Stranding is form of progress failure
    // where the monitor is unlocked but all the contending threads remain parked).
    // That is, at least one of the contended threads will periodically poll _owner.
    // One of the contending threads will become the designated "Responsible" thread.
    // The Responsible thread uses a timed park instead of a normal indefinite park
    // operation -- it periodically wakes and checks for and recovers from potential
    // strandings admitted by 1-0 exit operations.   We need at most one Responsible
    // thread per-monitor at any given moment.  Only threads on cxq|EntryList may
    // be responsible for a monitor.
    //
    // Currently, one of the contended threads takes on the added role of "Responsible".
    // A viable alternative would be to use a dedicated "stranding checker" thread
    // that periodically iterated over all the threads (or active monitors) and unparked
    // successors where there was risk of stranding.  This would help eliminate the
    // timer scalability issues we see on some platforms as we'd only have one thread
    // -- the checker -- parked on a timer.

    if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
        // Try to assume the role of responsible thread for the monitor.
        // CONSIDER:  ST vs CAS vs { if (Responsible==null) Responsible=Self }
        Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
    }

    // The lock have been released while this thread was occupied queueing
    // itself onto _cxq.  To close the race and avoid "stranding" and
    // progress-liveness failure we must resample-retry _owner before parking.
    // Note the Dekker/Lamport duality: ST cxq; MEMBAR; LD Owner.
    // In this case the ST-MEMBAR is accomplished with CAS().
    //
    // TODO: Defer all thread state transitions until park-time.
    // Since state transitions are heavy and inefficient we'd like
    // to defer the state transitions until absolutely necessary,
    // and in doing so avoid some transitions ...

    TEVENT (Inflated enter - Contention) ;
    int nWakeups = 0 ;
    int RecheckInterval = 1 ;
    // 此处为锁竞争循环,未竞争到锁则挂起线程,等待唤醒后从挂起处继续执行
    for (;;) {

        if (TryLock (Self) > 0) break ;
        assert (_owner != Self, "invariant") ;

        if ((SyncFlags & 2) && _Responsible == NULL) {
           Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
        }

        // park self
        if (_Responsible == Self || (SyncFlags & 1)) {
            TEVENT (Inflated enter - park TIMED) ;
            Self->_ParkEvent->park ((jlong) RecheckInterval) ;
            // Increase the RecheckInterval, but clamp the value.
            RecheckInterval *= 8 ;
            if (RecheckInterval > 1000) RecheckInterval = 1000 ;
        } else {
            TEVENT (Inflated enter - park UNTIMED) ;
            Self->_ParkEvent->park() ;
        }

        if (TryLock(Self) > 0) break ;

    // The lock is still contested.
    // Keep a tally of the # of futile wakeups.
    // Note that the counter is not protected by a lock or updated by atomics.
    // That is by design - we trade "lossy" counters which are exposed to
    // races during updates for a lower probe effect.
    TEVENT(Inflated enter - Futile wakeup);
    // This PerfData object can be used in parallel with a safepoint.
    // See the work around in PerfDataManager::destroy().
    OM_PERFDATA_OP(FutileWakeups, inc());
    ++nWakeups;

        // Assuming this is not a spurious wakeup we'll normally find _succ == Self.
        // We can defer clearing _succ until after the spin completes
        // TrySpin() must tolerate being called with _succ == Self.
        // Try yet another round of adaptive spinning.
        if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ;

        // We can find that we were unpark()ed and redesignated _succ while
        // we were spinning.  That's harmless.  If we iterate and call park(),
        // park() will consume the event and return immediately and we'll
        // just spin again.  This pattern can repeat, leaving _succ to simply
        // spin on a CPU.  Enable Knob_ResetEvent to clear pending unparks().
        // Alternately, we can sample fired() here, and if set, forgo spinning
        // in the next iteration.

        if ((Knob_ResetEvent & 1) && Self->_ParkEvent->fired()) {
           Self->_ParkEvent->reset() ;
           OrderAccess::fence() ;
        }
        if (_succ == Self) _succ = NULL ;

        // Invariant: after clearing _succ a thread *must* retry _owner before parking.
        OrderAccess::fence() ;
    }

    // Egress :
    // Self has acquired the lock -- Unlink Self from the cxq or EntryList.
    // Normally we'll find Self on the EntryList .
    // From the perspective of the lock owner (this thread), the
    // EntryList is stable and cxq is prepend-only.
    // The head of cxq is volatile but the interior is stable.
    // In addition, Self.TState is stable.

    assert (_owner == Self      , "invariant") ;
    assert (object() != NULL    , "invariant") ;
    // I'd like to write:
    //   guarantee (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
    // but as we're at a safepoint that's not safe.

    UnlinkAfterAcquire (Self, &node) ;
    if (_succ == Self) _succ = NULL ;

    assert (_succ != Self, "invariant") ;
    if (_Responsible == Self) {
        _Responsible = NULL ;
        OrderAccess::fence(); // Dekker pivot-point

        // We may leave threads on cxq|EntryList without a designated
        // "Responsible" thread.  This is benign.  When this thread subsequently
        // exits the monitor it can "see" such preexisting "old" threads --
        // threads that arrived on the cxq|EntryList before the fence, above --
        // by LDing cxq|EntryList.  Newly arrived threads -- that is, threads
        // that arrive on cxq after the ST:MEMBAR, above -- will set Responsible
        // non-null and elect a new "Responsible" timer thread.
        //
        // This thread executes:
        //    ST Responsible=null; MEMBAR    (in enter epilog - here)
        //    LD cxq|EntryList               (in subsequent exit)
        //
        // Entering threads in the slow/contended path execute:
        //    ST cxq=nonnull; MEMBAR; LD Responsible (in enter prolog)
        //    The (ST cxq; MEMBAR) is accomplished with CAS().
        //
        // The MEMBAR, above, prevents the LD of cxq|EntryList in the subsequent
        // exit operation from floating above the ST Responsible=null.
    }

    // We've acquired ownership with CAS().
    // CAS is serializing -- it has MEMBAR/FENCE-equivalent semantics.
    // But since the CAS() this thread may have also stored into _succ,
    // EntryList, cxq or Responsible.  These meta-data updates must be
    // visible __before this thread subsequently drops the lock.
    // Consider what could occur if we didn't enforce this constraint --
    // STs to monitor meta-data and user-data could reorder with (become
    // visible after) the ST in exit that drops ownership of the lock.
    // Some other thread could then acquire the lock, but observe inconsistent
    // or old monitor meta-data and heap data.  That violates the JMM.
    // To that end, the 1-0 exit() operation must have at least STST|LDST
    // "release" barrier semantics.  Specifically, there must be at least a
    // STST|LDST barrier in exit() before the ST of null into _owner that drops
    // the lock.   The barrier ensures that changes to monitor meta-data and data
    // protected by the lock will be visible before we release the lock, and
    // therefore before some other thread (CPU) has a chance to acquire the lock.
    // See also: http://gee.cs.oswego.edu/dl/jmm/cookbook.html.
    //
    // Critically, any prior STs to _succ or EntryList must be visible before
    // the ST of null into _owner in the *subsequent* (following) corresponding
    // monitorexit.  Recall too, that in 1-0 mode monitorexit does not necessarily
    // execute a serializing instruction.

    if (SyncFlags & 8) {
       OrderAccess::fence() ;
    }
    return ;
}

2.3 释放锁流程

在这里插入图片描述

objectMonitor.cppexit()函数为释放锁操作,其大致流程如下:

  1. 首先进行必要的锁重入判断,进行重入计数的计算
  2. 默认的退出策略中,首先设置_owner 为 NULL,也就是释放锁,此时如果没有等待锁的线程或已经有了候选的唤醒线程直接 return 返回,否则需要重新设置_owner为当前线程,因为之后操作 _cxq_EntryList队列以及唤醒线程需要重新获得锁
  3. 根据 QMode 的不同执行不同的唤醒策略,最终通过ObjectMonitor::ExitEpilog()函数唤醒线程,唤醒操作最终由unpark完成
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
   Thread * Self = THREAD ;
    // 如果当前线程未持有 Monitor
   if (THREAD != _owner) {
     if (THREAD->is_lock_owned((address) _owner)) {
       // Transmute _owner from a BasicLock pointer to a Thread address.
       // We don't need to hold _mutex for this transition.
       // Non-null to Non-null is safe as long as all readers can
       // tolerate either flavor.
       assert (_recursions == 0, "invariant") ;
       _owner = THREAD ;
       _recursions = 0 ;
       OwnerIsThread = 1 ;
     } else {
       // NOTE: we need to handle unbalanced monitor enter/exit
       // in native code by throwing an exception.
       // TODO: Throw an IllegalMonitorStateException ?
       TEVENT (Exit - Throw IMSX) ;
       assert(false, "Non-balanced monitor enter/exit!");
       if (false) {
          THROW(vmSymbols::java_lang_IllegalMonitorStateException());
       }
       return;
     }
   }
   // 当前线程持有 Monitor,如果_recursions次数不为0,自减
   if (_recursions != 0) {
     _recursions--;        // this is simple recursive enter
     TEVENT (Inflated exit - recursive) ;
     return ;
   }

   // Invariant: after setting Responsible=null an thread must execute
   // a MEMBAR or other serializing instruction before fetching EntryList|cxq.
   if ((SyncFlags & 4) == 0) {
      _Responsible = NULL ;
   }
  // 根据不同的 QMode,从 cxq 或 _EntryList 中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒
  // 该节点封装的线程,唤醒操作最终由unpark完成
    ...... 
}
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值