【并发编程】 --- 从JVM源码的角度进一步去理解synchronized关键字的原理

源码地址:https://github.com/nieandsun/concurrent-study.git

上篇文章《【并发编程】 — 从字节码指令的角度去理解synchronized关键字的原理》从字节码指令的角度讲解了synchronized关键字的原理,从中可以知道其实synchronized关键字真正锁的是锁对象关联的monitor对象,那

  • (1)这个monitor对象到底什么呢?
  • (2)monitorentor、monitorexit这两个字节码指令和monitor之间到底是什么关系呢?
  • (3)为什么很多资料上都说monitor是重量级锁呢?

本篇文章将来讲解一下这几个问题。


1 openjdk(hotspot)源码下载

源码地址:http://openjdk.java.net/ --> Mercurial --> jdk8 --> hotspot --> zip
当然如果下载不下来,也可以从我提供的源码里clone: https://github.com/nieandsun/concurrent-study.git


2 monitor对象简介

首先应该知道,monitor并不是我们java开发人员主动创建的一个对象。事实上可以这么理解:monitor并不是随着对象的创建而创建的,而是我们通过synchronized修饰符告诉JVM需要为我们的某个对象(锁对象)创建关联的monitor对象。

在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用C++写的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor主要数据结构如下:

 // initialize the monitor, exception the semaphore, all other fields
 // are simple integers or pointers
  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;//线程的重入次数
    _object       = NULL;//存储改monitor的对象
    _owner        = NULL;//拥有该monitor的线程
    _WaitSet      = NULL;//处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;//多线程竞争锁时的单向列表
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

对其中几个比较重要的变量进行解释如下:

  • (1)_owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程 释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程的安全。
  • (2)_cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资 源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指 向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
  • (3)_EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中。
  • (4)_WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。

锁对象与monitor中的_owner、_WaitSet以及_EntryList之间的关系可以用下图进行表示:

在这里插入图片描述


3 monitorenter、monitorexit与monitor之间的关系

monitorentermonitorexit 这两个字节码指令到底与monitor之间是什么样的关系呢?

其实并没有那么不可想象!!! 以monitorenter字节码指令为例,当JVM当发现要运行此字节码指令时,就会调用C++的 InterpreterRuntime::monitorenter函数,该函数的位于HotSpot虚拟机源码InterpreterRuntime.cpp中(src/share/vm/interpreter/interpreterRuntime.cpp),具体源码如下:

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); //JDK1.6及之后对锁进行的优化
  } 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");

对于重量级锁,monitorenter函数中会调用 ObjectSynchronizer::slow_enter方法
并最终调用 ObjectMonitor::enter方法(位于:src/share/vm/runtime/objectMonitor.cpp),源码如下:

void ATTR ObjectMonitor::enter(TRAPS) {
  // The following code is ordered to check the most common cases first
  // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
  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 ;
  }
 	//省略一些代码。。。。
    // TODO-FIXME: change the following for(;;) loop to straight-line code.
    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);
  }

这里暂时先不考虑JDK1.6及之后对synchronized关键字进行的优化。

以上代码的具体流程概括起来如下:

  • (1) 通过CAS尝试把monitor的owner字段设置为当前线程。 ---- 调用了内核函数 Atomic::cmpxchg_ptr
  • (2)如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行 recursions ++ ,记录重入的次数。
  • (3)如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获 得锁并返回。
  • (4) 如果获取锁失败,则等待锁的释放。

到这里我自认为作为java程序员就没必要再进一步去深究JVM源码了,因为从上面的介绍其实已经可以知道,当JVM遇到monitorenter指令时会调用C++的代码为当前线程去抢占锁对象对应的monitor的所有权。那可以想像当JVM遇到monitorexit指令时肯定也会调用C++的代码释放monitor的所有权,并唤醒其他线程(源码为src/share/vm/runtime/objectMonitor.cpp中的ObjectMonitor::exit方法, 有兴趣的可以自己去研究)。
做个小图进行总结一下:
在这里插入图片描述


4 为什么说monitor是重量级锁

我这里并不想去细究什么是用户态、内核态或者用户空间、内核空间 — 有兴趣的可以自行百度。
因此这里只简单给出两句话:

  • (1)争抢锁+释放锁的C++代码里会调用一些内核函数。

  • (2)简单理解就是,JVM调用了内核提供的函数,期间肯定要涉及到cpu从JVM(用户态)到内核态的来回切换 — 这种切换会带来大量的系统资源消耗,所以说monitor是一个重量级锁。


应该知道的事


  • (1)JDK1.6之前JVM遇到monitorenter字节码指令时会直接去抢monitor锁,也就是说无论被synchronized关键字修饰的代码块或方法是否正在并发执行都会涉及到用户态和内核态的切换 。
    • 但有研究表明程序大多数情况下都不会遇到并发的情况
      — 这就是JDK1.6之前synchronized关键字面临的最大的尴尬,
      或许这也是Doug Lea 最看不惯的地方 —> 下篇文章将讲一下Doug Lea在ReentrantLock中针对该问题的解决方案

  • (2)虽然JDK1.6对synchronized关键字进行了优化,但是应该要明确的是monitor锁仍然存在,也就是说使用synchronized关键字有可能最终还是要进行用户态和内核态的切换
    • 其实这个问题是避免不了的,因为线程挂起(park)和挂起线程的唤醒(unpark)本身就是在进行用户态和内核态的切换。
    • JDK1.6及之后对synchronized关键字的优化,本质上是对会出现并发问题的代码在抢锁时、在没有真正并发执行时等特殊时机进行的优化,以及锁的粒度等进行的优化 —》并不是说就杜绝了用户态和内核态的切换;
    • 当真正出现并发时,无论是Doug Lea搞得JUC还是synchronized关键字都不可避免的要对抢不到锁的线程进行挂起(park)、对被挂起的线程进行唤醒(unpark)等操作 — 从而会进行用户态和内核态的切换。

end

发布了225 篇原创文章 · 获赞 319 · 访问量 53万+

猜你喜欢

转载自blog.csdn.net/nrsc272420199/article/details/105278429
今日推荐