为了账号安全,请及时绑定邮箱和手机立即绑定

JDK成长记15:从0分析你不知道的synchronized底层原理(上)

标签:
Java

file

前几节你应该已经了解和掌握了Thread、ThreadLocal、Volatile这几个并发基础知识的底层原理。这一节,你可以跟我一起深入了解下synchronized关键字的底层原理和其涉及的基础知识。看完这篇成长记,你可以获取到如下几点:

synchronized预备知识:

  • 理解什么是CAS?
  • synchronized会形成几种锁的类型
  • HotspotJVM虚拟机Java对象内存中的布局结构是什么,markword是锁的关键字段?
  • 操作系统中用户态和内核态的资源操作和进程是什么意思?

synchronized核心流程及原理:

  • 从3个层面初步分析sychronized的核心流程和原理

好了,让我们一起开始吧!

HelloSychronized

HelloSychronized

我们来写一个多线程i++的程序,体验一下,多线程如果是并发的修改一个数据,会有什么样的线程并发安全问题。

刚才说过了,volatile,解决的对一个共享数据,有人写,有人读,多个线程并发读和写的可见性的问题,而多个线程对一个共享数据并发的写,可能会导致数据出错,产生原子性的问题。

volatile为什么不能保证原子性? 从JMM内存模型就可以看出来,多个线程同时修改一个变量,都是在自己本地内存中修改,volatile只是保证一个线程修改,另一个线程读的时候,发起修改的线程是强制刷新数据主存,过期其他线程的工作内存的缓存,没法做到多个线程在本地内存同时写的时候,限制只能有一个线程修改,因为线程自己修改自己内存的数据没有发生竞争关系。而且之后会给各自写入主内存,当然就保证不了只能有一个线程修改主内存的数据,做不到原子性了。

为了解决这个问题,可以使用syncrhonized给修改这个操作加一把锁,一旦说某个线程加了一把锁之后,就会保证,其他的线程没法去读取和修改这个变量的值了,同一时间,只有一个线程可以读这个数据以及修改这个数据,别的线程都会卡在尝试获取锁那儿。这样也就不会出现并发同时修改,数据出错,原子性问题了。

synchronized锁一般有两类,一种是对某个实例对象来加锁,另外一种是对这个类进行加锁。相信大家很熟悉了,这里用一个Hello synchronized的小例子,举一个简单对象加锁的例子。

代码如下:

  public class HelloSynchronized {
    public static void main(String[] args) {
      Object o = new Object();
      synchronized (o){
      }
    }
  }

对类加锁和对实例对象的更多例子这里就不举例了,我们更多的是研究synchronized它的底层原理。基本的使用相信你一定可以自己学习到。

在分析sychronized原理期间,需要不断的补充一些基础知识。

学习sychronized先决条件(Prerequisites)

学习sychronized先决条件(Prerequisites)

  • sychronized锁的概念

在JDK 早期 sychronized 使用的时候,直接创建的重量级锁,性能很不好。

在之后JDK新的版本中,sychronized优化了锁,分为了4种,无锁态、偏向锁、自旋锁(轻量锁)、重量锁,会根据情况自动升级锁。

这四种锁分别表示什么意思呢?

无锁态表示第一次对刚创建的对象或者类加锁时的状态。我发现只有一个线程在操作代码块的资源,压根不需要加锁。此时会处于无锁态。

偏向锁,类似于贴标签,表示这个资源暂时属于某个线程,偏向它所有了。打个比方,就好比一个座位只能做一个人,你坐下后,在座位上贴上了你自己的标签。别人发现已经有标签了,肯定就不会在坐了。

**轻量锁(自旋锁):**轻量锁,底层是CAS自旋的操作,所以也叫自旋锁。这里简单普及下自旋CAS的操作流程,之后将Aotmic类的时候会仔细讲下。CAS自旋流程如下:

file

最后我们来聊下什么是重量级锁?这又要牵扯另一个知识了。在Linux操作系统层面,由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级用户态和内核态。用于表示进程运行时所处状态。

你可以简单理解,一个程序启动后会有对应的进程,它们操作的资源分为两种,属于用户态的资源或者内核态的资源。

用户态是不能直接操作内核态中资源的,只能通知内核态来操作。这个在硬件级别也有对应的指令级别(比如Intel ring0-ring3级别的指令,ring0级别一般对应的就是用户态进程可以操作的指令,ring3对应的是内核态进程可以发起的指令)。

如下图所示:

file

这个和synchronized有什么关系呢?因为synchronized加重量级锁的操作,是对硬件资源的锁指令操作,所以肯定是需要处于内核态的进程才可以操作,JVM的进程只是处于用户态的进程,所以需要向操作系统申请,这个过程肯定会很消耗资源的。

比如,synchronized的本质是JVM用户空间的一个进程(处于用户态)向操作系统(内核态)发起一个lock的锁指令操作 。

C++代码如下:

  //Adding a lock prefix to an instruction on MP machine
  \#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; local; 1 : "

如下图右边所示:

file

  • sychronized锁状态的记录

了解了sychronized的锁的几种类型后,怎么标识是什么样的synchronized锁呢?这个就要聊到Java的对象在JVM的内存中的结构了。不同虚拟机结构略有差别,这里讲一下HotSpot虚拟机中的对象结构:

file

synchronized锁状态的信息就记录在markword中。markword在64位的操作系统上,8字节,64位大小的空间的区域。

不同的锁的标记在如下图所示:

file

这个表你不用背下来,你只要知道,synchronized的轻量锁和重量锁通过2位即可以区分出来,偏向锁和无锁需要3位。

有了上面的基础知识后,就可以开始研究synchronized的底层原理了。

字节码层面的synchronized

synchronized

sychronized在Java代码层面就如上面Hello Synconized那个最简单的例子所示,我们来看下它的字节码层面是什么样子的?

上面main方法的字节码如下:

0 new #2 <java/lang/Object>
 3 dup
 4 invokespecial #1 <java/lang/Object.<init>>
 7 astore_1
 8 aload_1
 9 dup
 10 astore_2
 11 monitorenter
 12 aload_2
 13 monitorexit
 14 goto 22 (+8)
 17 astore_3
 18 aload_2
 19 monitorexit
 20 aload_3
 21 athrow
 22 return

new、dup、invokespecial、 astore_1这些指令是学习volatile的时候你应该很熟悉了。我这里需要关注的是另外 2个核心的JVM指令:monitorenter、monitorexit

这个表示sychronized加锁的同步代码块的进入和退出。为什么有两个monitorexit呢?一个是正常退出,一个抛出异常也会退出释放锁。

JVM层面的synchronized

JVM层面的synchronized

那么,当 JVM的HotSpot实现中,当遇到这两个JVM指令,又是如何执行的呢?让我们来看一下。

在JVM HotSpot的C++代码实际执行过程中,执行了一个InterpreterRuntime:: monitorenter方法,代码如下:

  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);
   } 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");

  \#ifdef ASSERT

   thread->last_frame().interpreter_frame_verify_monitor(elem);

  \#endif

  IRT_END

你可以看下上面的方法的脉络(不懂C++也没有关系,懂if-else就行)。它的核心有两个if。

第一个if根据变量名字PrintBiasedLockingStatistics可以判断出应该是打印偏向锁的统计信息,明显不是最重要的。

第二个if同理,UseBiasedLocking表示了是否使用了偏向锁,如果是调用了ObjectSynchronizer::fast_enter否则

ObjectSynchronizer::slow_enter。

很明显,第二个if中是synchronized加锁的核心代码。我们还需要继续看下它们的脉络。

代码如下:synchronizer.cpp

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {

    if (UseBiasedLocking) {
      if (!SafepointSynchronize::is_at_safepoint()) {
       BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);

       if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
       }

      } else {
       assert(!attempt_rebias, "can not rebias toward VM thread");
       BiasedLocking::revoke_at_safepoint(obj);
      }
      assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
    }
  
    slow_enter (obj, lock, THREAD) ;

    }

可以看到fast_enter方法,核心脉络除了取消偏向和重新偏向的逻辑(从变量明和注释可以看出来,这里暂时不重要,先忽略),最后核心脉络还是调用了slow_enter方法。让我们来看下:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {

   markOop mark = obj->mark();
   assert(!mark->has_bias_pattern(), "should not see bias pattern here"); 

   if (mark->is_neutral()) {
    // Anticipate successful CAS -- the ST of the displaced mark must
    // be visible <= the ST performed by the CAS.
    lock->set_displaced_header(mark);
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
     TEVENT (slow_enter: release stacklock) ;
     return ;
    }
    // Fall through to inflate() ...
   } else

   if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    lock->set_displaced_header(NULL);
    return;
   }


  \#if 0
   // The following optimization isn't particularly useful.
  if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
    lock->set_displaced_header (NULL) ;
    return ;
   }
  \#endif   

   // The object header will never be displaced to this lock,
   // so it does not matter what the value is, except that it
   // must be non-zero to avoid looking like a re-entrant lock,
   // and must not look locked either.
   lock->set_displaced_header(markOopDesc::unused_mark());
   ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);

  }

上面这一段是sychronized加锁,核心中的核心,可以发现很多有意思的地方:

1) 从注释可以看出,锁会有膨胀过程,对象头会记录锁的相关信息。

2) Atomic::cmpxchg_ptr体现了ompare and exchange (CAS)操作,是轻量级锁。

3) mark->has_locker() && THREAD->is_lock_owned((address)mark->locker()体现了synchronized是可重入锁

4) 最后的ObjectSynchronizer::inflate意思为膨胀为重量级锁。

C++的代码有很多细节和知识,你开始学习的时候不要想着全部搞清楚,一定要有之前学到的思想,先脉络后细节。搞清楚脉络再说研究细节的部分。

所以,通过初步看过synchronized的HotSpot C++代码实现,重点的脉络就是锁升级的过程和原理,接下来重点分析一下这个过程。

synchronized锁升级的过程

synchronized锁升级的过程

前面通过从字节码层面到JVM层面初步了解了synchronized的实现,结合之前说的sychronized的锁的几种类型。最终可以分析出来synchronized锁会有一个升级的过程。过程如下图所示:

file

这个图非常重要,大家一定要牢记住。下一节会花费整整一节来讲在这个图。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
1
获赞与收藏
9

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消