JVM原理与实现——Synchronized关键字

本文出自:【InTheWorld的博客】 (欢迎留言、交流)

openJDK_gitar

在多线程的Java程序中,Synchronized关键字是经常出现的。这篇文章里,我们就来研究一下它的实现原理。比如以下的示例程序:

public class SynchronizedTest {
    int syncFunc() {
        synchronized(this) {
            int a = 0;
            return a;
        }
    }
}

对应的字节码如下:

Compiled from "SynchronizedTest.java"
public class SynchronizedTest {
  public SynchronizedTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  int syncFunc();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: iconst_0
       5: istore_2
       6: iload_2
       7: aload_1
       8: monitorexit
       9: ireturn
      10: astore_3
      11: aload_1
      12: monitorexit
      13: aload_3
      14: athrow
    Exception table:
       from    to  target type
           4     9    10   any
          10    13    10   any
}

Java编译器为synchronized关键字生成了monitorenter和monitorexit字节码。这两个关键字把临界区包裹起来,实现了函数的线程安全。所以我们需要研究一下monitorenter和monitorexit字节码的工作原理。换句话说,就是要找到JVM是如何解释执行这两个字节码的。

谈到Java解释器就说来话长了,因为JVM中有多个Interpreter的实现。虽然它们的效率有比较大的差异,但可用性都是有保证的,毕竟Java都二十多年了。这里为了简单起见,我就拿比较初级的BytecodeInterpreter分析了。这个字节码解释器有一个重要的run()方法,它就是实现字节码解析和执行的函数。虽然功能听起来挺高大上的,但实际上就是一个很大的switch语句。不信你来看代码!

  while (1)
  {
      opcode = *pc;
      switch (opcode)
      {
      CASE(_nop):
          UPDATE_PC_AND_CONTINUE(1);
          /* Push miscellaneous constants onto the stack. */
      CASE(_aconst_null):
          SET_STACK_OBJECT(NULL, 0);
          UPDATE_PC_AND_TOS_AND_CONTINUE(1, 1);
          /* Push a 1-byte signed integer value onto the stack. */
      CASE(_monitorenter): {
        oop lockee = STACK_OBJECT(-1);
        // derefing's lockee ought to provoke implicit null check
        CHECK_NULL(lockee);
        // find a free monitor or one already allocated for this object
        // if we find a matching object then we need a new monitor
        // since this is recursive enter
        BasicObjectLock* limit = istate->monitor_base();
        BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
        BasicObjectLock* entry = NULL;
        while (most_recent != limit ) {
          if (most_recent->obj() == NULL) entry = most_recent;
          else if (most_recent->obj() == lockee) break;
          most_recent++;
        }
        if (entry != NULL) {
          entry->set_obj(lockee);
          markOop displaced = lockee->mark()->set_unlocked();
          entry->lock()->set_displaced_header(displaced);
          if (Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
            // Is it simple recursive case?
            if (THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
              entry->lock()->set_displaced_header(NULL);
            } else {
              CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
            }
          }
          UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
        } else {
          istate->set_msg(more_monitors);
          UPDATE_PC_AND_RETURN(0); // Re-execute
        }
      }
  }

这段代码被各种删减之后,你会发现它是如此的简单。while循环加switch,确定不是《程序设计基础》第二章的例题?虽然这段代码已经非常简单了,但是你依然可以清晰的看到我们的主角——_monitorenter,这个重要的字节码。它的兄弟字节码_moniterexit,我们先按下不表。等我们搞清楚_moniterenter,你会发现_moniterexit只是一个逆向操作而已。

1. lightweight locked(轻度锁)

然而这个moniterenter却很不简单呀!先看if (entry != NULL)之前的代码,这段代码的功能就是尝试分配一个可用的entry。关于这个entry的使用,就到了if语句之后的部分了。这段代码看似简单,但还是需要一些背景知识才可能理解的。首先需要介绍的是Mark Word,这是每一个Object对象都拥有的一个域。在32位的机器上,Mark Word的长度也是32位,它的具体含义如下图:

mark_word

图中的每一行代表一个独立状态,状态之间是互斥的。第一种状态是Object的正常状态,第二种状态是轻度锁的状态第三种是moniter重度锁状态,第五种是偏向锁状态。我们首先把重点放在前三种状态上,来研究Object的锁是如何工作的。为了方便理解,贴两图出来。下面这张图是进入if语句之前的状态:

mark_word_normal

这里的Lock record就是刚刚提到的entry变量,而Object对象头部的mark word也表示它还是个“单身汉”。接下来,当前线程会尝试进入轻度锁状态。这个过程如下图所示:

mark_word_cas

if语句内部的前三行代码实际上就是完成了Lock record的赋值,这其中包括根据Object的mark word设置header,以及把entry的obj(也就是owner)指向Object。然后就到了激动人心的时刻,该CAS出场了,能不能拿到轻量锁就在此一举了!

这个CompareAndSwap的作用就是——比较Object的mark word和Lock record的header是否相同。如果相同,那么就把entry的地址赋值给mark word;如果不同,说明Object已经名花有主了,然后尝试其他方式。这里有一个非常有趣的trick,值得和大家一说。为什么轻量锁状态的tag bits是00,而不是其他值呢?答案就是,在CAS的成功操作中,会把entry的地址赋值给mark word。一般来说,地址值的低两位会有多种组合,但是如果我们在构造entry是按4字节对齐,那么它地址的低两位就一定是00。而且4字节对齐会更容易实现,所以就把tag bits规定为这样了。

2. monitor重度锁

前面的代码中,如果CAS成功,那么就直接进入后续的字节码执行了,所以我们要研究CAS失败的情况。如果失败了,首先会检查是不是当前线程重入了,如果是,那么也相当于成功了。如果判断也不是当前线程重入,那就到了InterpreterRuntime::monitorenter(THREAD, entry), handle_exception)的表演时刻了。

实际上InterpreterRuntime::monitorenter()方法也会再尝试一次CAS,因为一段时间之后,锁可能已经被释放了。如果还是失败,就没办法了——进入monitor重度锁吧!这个操作是由ObjectMonitor::enter()完成的。

void ATTR ObjectMonitor::enter(TRAPS) {
   Thread * const Self = THREAD ;
   void * cur ;
  Atomic::inc_ptr(&_count);

  EventJavaMonitorEnter event;

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

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

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

    Self->set_current_pending_monitor(this);

    // TODO-FIXME: change the following for(;;) loop to straight-line code.
    for (;;) {
      jt->set_suspend_equivalent();

      EnterI (THREAD) ;

      if (!ExitSuspendEquivalent(jt)) break ;

      exit (false, Self) ;

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

   Atomic::dec_ptr(&_count);
   assert (_count >= 0, "invariant") ;
   Self->_Stalled = 0 ;

  if (ObjectMonitor::_sync_ContendedLockAttempts != NULL) {
     ObjectMonitor::_sync_ContendedLockAttempts->inc() ;
  }
}

这个函数会在for循环中不断检查是否拿到了Moniter锁,如果拿到了就成功的执行完了这个monitorenter字节码了。这里就先不介绍monitorexit了。还有就是偏向锁也没有介绍,等后续有需要的话再写了。

 

参考:

http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf

已有8条评论 发表评论

  1. chrisliu /

    线程在调用synchronized修饰的代码块或者方法,首先获取锁,并且是先获取lightweight locked,失败了再获取monitor重度锁?

    1. lshw4320814 / 本文作者

      更完整和准确的说,应该是偏向锁-》轻量锁-》monitor重锁这样一个顺序。从左到右,锁的消耗递增。

  2. chrisliu /

    还有一个疑问就是函数:
    void ATTR ObjectMonitor::enter(TRAPS);这个函数会在for循环中不断检查是否拿到了Moniter锁;
    我原来是这样理解的:某个线程去获取Moniter锁,如果没有获取到(说明被其他线程获取了)就应该阻塞了呀。从void ATTR ObjectMonitor::enter(TRAPS)函数来看,应该是个某个线程没有获取到Moniter锁(被其他线程获取了)然后会一直循环获取?

    1. lshw4320814 / 本文作者

      确实会不断获取,但不是死循环,它会把自己挂起来,被唤醒后再检查。这部分逻辑我没有细看,但我觉得应该是这样。

      1. chrisliu /

        感谢回复!

  3. 郝黎阳 /

    有个问题:假设线程A在执行同步操作的过程中没有竞争,且需要对同一个对象多次重入,现在知道第一次CAS操作成功后,会在当前栈帧中新建一个lcok record,请问第二次重入时还会继续创建一个新的lock record吗?

    1. lshw4320814 / 本文作者

      你看下面这三句话,以及之后的6行代码。
      // find a free monitor or one already allocated for this object
      // if we find a matching object then we need a new monitor
      // since this is recursive enter
      结论是,即使是重入也需要新建一个lock record。在cmpxchg_ptr这里也能看出来,在cas失败的时候,也还是会先判断该线程是不是已经拥有锁了。这里看起来确实有点不太必要,我也没想明白这样做的原因。

  4. 往之 /

    两个地方不是很明白:
    1.关于这句语句的含义markOop displaced = lockee->mark()->set_unlocked();
    这是直接将锁对象的markword设置为无锁状态吗?
    2.如果这句Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced)CAS成功了,那该线程就直接获得锁了吗? 偏向锁的逻辑呢?不应该是先尝试偏向锁,再轻量级锁吗?
    被这两个问题困扰了很久,期待你的回复。

发表评论