问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

人人都会的synchronized锁升级,你真正从源码层面理解过吗?

创作时间:
作者:
@小白创作中心

人人都会的synchronized锁升级,你真正从源码层面理解过吗?

引用
1
来源
1.
https://developer.aliyun.com/article/1593004

本文将从源码层面深入解析Java中的synchronized锁升级机制,包括偏向锁、轻量级锁、自旋锁和重量级锁的实现原理。通过分析JVM源码,帮助读者全面理解锁的存储方式、加锁过程以及锁升级的触发条件。

一、背景

synchronized是Java语言实现多线程间同步的技术,其语法简单易用,但其背后的实现原理却较为复杂。由于锁的实现原理在JVM层面通过C++语言实现,对于Java开发者来说理解起来有一定难度。此外,锁升级原理也是互联网公司面试中常见的题目之一。

本文将结合OpenJDK 8(jdk8-b120)的源码,从底层实现的角度深入分析synchronized的原理。通过阅读本文,读者将能够从源码层面理解锁升级的具体过程。

二、带着几个疑问去分析源码

在深入分析源码之前,我们先明确几个关键问题:

  1. 偏向锁、轻量级锁、自旋锁、重量级锁的锁信息是如何存储的?存储在哪里?
  2. 如何判断一个对象是否被锁?自旋锁的具体实现机制是什么?

开始分析源码

首先我们找加锁入口

如何查看JVM的源码入口呢?我们可以先看下synchronized的字节码。锁代码块的字节码和锁方法的字节码有所不同,通过字节码关键字,我们从JDK源码可以找到bytecodeInterpreter.cpp

没错,bytecodeInterpreter.cpp就是JVM解释字节码的代码逻辑。

锁是怎么存储的

了解synchronized锁,我们首先要知道锁存储在哪里?我们锁的操作目标对象是一个对象,这个对象要么是对象实例,要么是类对象,我们要知道锁是如何存储的,要先了解下面几个类。先有一个印象,后面加锁流程会涉及到。

锁定义
基础锁对象定义:轻量级锁用到
对象头(存储锁标记)定义:偏向锁,轻量级锁(栈锁),重量级锁都会用到
对象头存储:包含2位锁标记
监视器ObjectMonitor(重量级锁使用)

在JVM的代码层,加锁逻辑分为快速和慢速加锁两个阶段,下面我们一个一个分析。

快速加锁阶段:偏向锁

JVM代码段偏向锁是快速加锁,所谓快速加锁阶段,就是尝试加偏向锁的过程,加锁逻辑在ObjectSynchronizer::fast_enter方法。

上面就是调用偏向锁对象加偏向锁,那么偏向锁怎么加的呢?

看到这里就是加偏向锁成功了。现在用下面的图总结下,偏向锁怎么算加锁成功。

到这里,快速加锁阶段完成,也就是偏向锁加锁完成:通过一次CAS操作设置对象头里面的加锁线程ID,同时设置最低3位的锁标记位为101,这也是偏向锁存储在哪的答案。

如果快速加锁失败,则会进入慢速加锁过程,慢速加锁包含轻量级锁、自旋锁、重量级锁加锁过程。

慢速加锁阶段1: 轻量级锁加锁过程

慢速加锁过程包括轻量级锁和重量级锁,先看轻量级锁加锁过程,加锁逻辑在ObjectSynchronizer::slow_enter

轻量级锁加锁过程,首先将对象头复制到线程的锁对象上,然后通过CAS将锁对象设置到对象头markword上。

看到到这里,轻量级锁加锁流程已经完成,首先复制一份加锁对象对象头markword到线程私有的BasicLock对象上,再通过CAS操作将锁对象设置到加锁目标对象对象头。所以轻量级锁存储会用到BasicLock和锁目标对象的对象头markword,需要注意的是BasicLock每个线程独有。

慢速加锁(锁膨胀)过程2:自旋锁锁加锁过程

ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);

第一步,获取ObjectMonitor对象,也就是自旋锁和重量级锁都是基于ObjectMonitor完成的。

我们以对象头上已经有其他线程加了轻量级锁为前提继续看源码。

//获取一个对象监视器对象
ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
   
   
  for (;;) {
   
   
      // CASE: 已经是轻量级锁的情况
      if (mark->has_locker()) {
   
   
          ObjectMonitor * m = omAlloc (Self) ;
          m->Recycle();
          m->_Responsible  = NULL ;
          m->OwnerIsThread = 0 ;
          m->_recursions   = 0 ;
          m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; 
          markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(),                                                              object->mark_addr(), mark) ;
          if (cmp != mark) {
   
   
             omRelease (Self, m, true) ;
             continue ;       // Interference -- just retry
          }
          markOop dmw = mark->displaced_mark_helper() ;
          assert (dmw->is_neutral(), "invariant") ;
          // Setup monitor fields to proper values -- prepare the monitor
          m->set_header(dmw) ;
          m->set_owner(mark->locker());
          m->set_object(object);
          //膨胀完成,设置膨胀状态
          object->release_set_mark(markOopDesc::encode(m));
          return m ;
      }
      // CASE: 无锁情况
      assert (mark->is_neutral(), "invariant");
      ObjectMonitor * m = omAlloc (Self) ;
      m->set_header(mark);
      return m ;
  }
}

这里获取到监视器对象,此时对象头已经设置为膨胀状态,为后续获取自旋锁和重量级锁做准备。这里注意获取到的监视器对象是所有线程共享的。

接着进入加锁逻辑在ObjectMonitor::enter(TRAPS)

  1. 重入锁或者当前线程加锁的情况
  2. 自旋锁

如果不是重入,且有其他线程抢占锁,开始尝试自旋转加锁。

首先尝试固定次数自旋
接着尝试自适应自旋
自旋锁,包含固定次数自旋和自适应次数自旋, 自适应自旋会根据自旋成功失败增加或者减少下次自旋次数。

我们可以发现自旋锁加锁是通过ObjectMonitor存储的。

慢速加锁过程3:重量级锁加锁过程

首先修改线程状态为阻塞状态。
接着再尝试自旋加锁,如果自旋加锁失败,线程park阻塞
升级为重量级锁后,加锁失败的线程进入阻塞状态,并进入等待队列等待被唤醒。
升级为重量级锁后,对象头的分布情况:

三、总结

本文从源码分析synchronized锁升级的过程,包含偏向锁,轻量级锁,自旋锁,重量级锁。最后对文章开头的疑问做一次回答作为文章的总结。

1、偏向锁,轻量级锁,自旋锁,重量级锁,加锁信息是怎么存储的?存在哪里?

偏向锁存储在加锁目标对象的对象头上。分布存储加偏向锁成功的线程id和锁标记。
轻量级锁存储在加锁线程独有的BasicLock对象,以及加锁目标对象的对象头markword上
自旋锁和重量级锁存储在ObjectMonitor对象,等待锁的线程存储在ObjectMonitor的队列上,锁标记同样存储在加锁对象的对象头上

2、怎么知道一个对象被锁了?自旋锁什么时候怎么做的?

通过加锁对象头markword可以知道对象当前加了哪种锁。
自旋锁是获取监视器对象后,再自旋加锁的,有固定次数自旋和自适应自旋两种机制,和重量级锁一样通过ObjectMonitor的_owner字段存储加锁成功的线程指针。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号