java多线程(4) - Moniter的实现原理

Moniter是什么

我们可以把监视器理解为包含一个特殊的房间的建筑物,这个特殊房间同一时刻只能有一个客人。如果一个顾客想要进去这个房间,就需要在走廊(Entry Set)排队,调度器将基于某个标准来选择排队的客户进入房间。如果用户暂时因为其他事情无法脱身,那么就会被送到等待室(Wait Set)。

监视器是一个用来监视这些线程进入特殊的房间的,它的义务是保证(同一时刻)只有一个线程可以访问被保护的数据和代码。

Monitor其实是一种同步机制,通常被描述为一个对象,其主要特点是:

对象的所有方法都被互斥执行。
通常提供singal机制:允许争持有“许可”的线程暂时放弃“许可”,等待某个谓词成真。条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让它可以重新去获得运行许可。

监视器的实现

HotSpot中基于C++由ObjectMoniter实现,主要数据结构是:

  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

其中的关键属性:

  • _owner:指向持有ObjectMoniter对象的线程。
  • _WaitSet:存放处于wait状态的线程队列。
  • _EntryList:存放处于等待锁block状态的线程队列。
  • _recursions:锁的重入次数。
  • _count:用来记录该线程获取锁的次数。

当多个线程同时访问一段同步代码,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时_count加一,即获得对象锁。

若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null,_count自减,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor锁并复位变量的值,以便其他线程进入获取monitor锁。
monitor

获得锁

ObjectMoniter中的实现:

  void ATTR ObjectMonitor::enter(TRAPS) {
  Thread * const Self = THREAD ;
  void * cur ;
  //通过CAS尝试把monitor的`_owner`字段设置为当前线程
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  //获取锁失败
  if (cur == NULL) {         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 ;
  }

  // 省略部分代码。
  // 通过自旋执行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 (Self) ;

  jt->java_suspend_self();
}
}

lockenter

void ATTR ObjectMonitor::exit(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;
     }
   }
    // 如果_recursions次数不为0.自减
   if (_recursions != 0) {
     _recursions--;        // this is simple recursive enter
     TEVENT (Inflated exit - recursive) ;
     return ;
   }

   //省略部分代码,根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒该节点封装的线程,唤醒操作最终由unpark完成。

lockexit

总结

实际上,在JDK1.6之前,synchronized的实现才会直接调用ObjectMoniter的enter和exit,这种锁被称为重量级锁,因为Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,就得从用户态切换到内核态,因此状态装换需要花费很多CPU时间,对于代码简单的同步块状态转换消耗的时间可能更多,所以说synchronized是java语言中一个重量级锁。

JDK1.6之后对锁进行了很多优化,进而出现轻量级锁、偏向锁、锁消除、适应性自旋锁、锁粗化。

comments powered by Disqus