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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: 在多线程Java程序中,`Synchronized`关键字用于确保线程安全。本文深入探讨其工作原理,通过分析字节码`monitorenter`和`monitorexit`,解释JVM如何实现同步机制。文章展示了`Synchronized`方法的编译结果,并详细解析了轻量锁和重度锁的实现过程,包括Mark Word的状态变化及CAS操作的应用。最后简要介绍了`ObjectMonitor::enter()`函数在获取重度锁时的作用。

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

aspectj

代码解读

复制代码

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

   对应的字节码如下:

yaml

代码解读

复制代码

Compiled from "SynchronizedTest.java"
public class SynchronizedTest {
  public SynchronizedTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()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语句。不信你来看代码!

reasonml

代码解读

复制代码

  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位,它的具体含义如下图:

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

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

   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()完成的。

rust

代码解读

复制代码

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了。还有就是偏向锁也没有介绍,等后续有需要的话再写了。


转载来源:https://juejin.cn/post/6844903486799216647

相关文章
|
5月前
|
存储 安全 Java
(二) 彻底理解Java并发编程之 Synchronized关键字实现原理剖析
Synchronized 关键字(互斥锁)原理,一线大厂不变的面试题,同时也是理解 Java 并发编程必不可少的一环!其中覆盖的知识面很多,需要理解的点也很多,本文会以相关书籍和结合自己的个人理解,从基础的应用范围到底层深入剖析的方式进行阐述,如果错误或疑问欢迎各位看官评论区留言纠正,谢谢!
117 0
|
5月前
|
微服务
多线程内存模型问题之在单例模式中,volatile关键字的作用是什么
多线程内存模型问题之在单例模式中,volatile关键字的作用是什么
|
7月前
|
安全 Java 编译器
Java多线程基础-6:线程安全问题及解决措施,synchronized关键字与volatile关键字(一)
线程安全问题是多线程编程中最典型的一类问题之一。如果多线程环境下代码运行的结果是符合我们预期的,即该结果正是在单线程环境中应该出现的结果,则说这个程序是线程安全的。 通俗来说,线程不安全指的就是某一代码在多线程环境下执行会出现bug,而在单线程环境下执行就不会。线程安全问题本质上是由于线程之间的调度顺序的不确定性,正是这样的不确定性,给我们的代码带来了很多“变数”。 本文将对Java多线程编程中,线程安全问题展开详细的讲解。
107 0
|
7月前
|
安全 Java 调度
Java多线程- synchronized关键字总结
Java多线程- synchronized关键字总结
53 0
|
7月前
|
Java
Java中的线程同步:synchronized关键字的深度解析
【4月更文挑战第14天】在多线程环境下,线程同步是一个重要的话题。Java提供了多种机制来实现线程同步,其中最常用且最重要的就是synchronized关键字。本文将深入探讨synchronized关键字的工作原理,使用方法以及注意事项,帮助读者更好地理解和使用这一重要的线程同步工具。
|
7月前
|
存储 安全 Java
深入理解Java中的Synchronized关键字
深入理解Java中的Synchronized关键字
深入理解Java中的Synchronized关键字
|
7月前
|
Java
多线程与并发,Java中的synchronized关键字的作用是什么?
多线程与并发,Java中的synchronized关键字的作用是什么?
59 1
|
存储 缓存 Java
并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则
并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则
64 0
|
Java
synchronized关键字的底层原理你了解吗?
synchronized关键字是Java中用于实现线程同步的关键字,它可以用于修饰方法或代码块。synchronized关键字的底层原理涉及到Java内存模型和对象监视器的概念。
106 0
|
存储 缓存 安全
【Java并发编程】Synchronized关键字实现原理(二)
【Java并发编程】Synchronized关键字实现原理