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

简介: 本文结合Jvm源码层面分析synchronized锁升级原理,可以帮助读者从本质上理解锁升级过程,从锁如何存储,存储在哪的疑问出发,一步一步彻底剖析偏向锁,轻量级锁,自旋锁,重量级锁的原理和机制。

大家好,本博客致力于分享互联网领域的各种技术干货,欢迎关注我们一起交流一起学习哦!

一、背景

synchronized是Java语言实现多线程间同步的技术,它使用语法非常简单。但是它的原理确难倒了大多数Java工程师,由于它的实现原理在Jvm层面通过C++语言实现,对于Java主语言的开发者来说不是特别友好,而且锁升级原理是互联网公司面试常问的题目之一,你遇到这个问题的时候有没有和面试官讲清楚呢?如果你自己看过源码,我相信一定能和面试官好好说清楚,刚好最近花了两三周时间研读Jvm的源码,怎么给其他人讲清楚是一个比较难的事情,坚持有输入一定要有输出的理念,本文特地将学习到的记录下来,本文将结合Jvm源码层面来分析synchronized的原理,我相信看了源码和没看源码的理解的是不一样的,想从源码层面理解锁升级的同学快来一起探索synchronized的源码吧。

image.png

说明:本文基于openjdk 8源码:jdk8-b120

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

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

可能很多人只知道对象头,今天看了源码后,你会发现没这么简单,加锁还会用到其他对象。

2、怎么知道一个对象被锁了?自旋锁到底是怎么做的?

开始分析源码

首先我们找加锁入口:

如何查看jvm的源码入口呢?我们可以先看下synchronized的字节码。

锁代码块的字节码:

image.png

锁方法的字节码:

image.png

通过字节码关键字,我们从Jdk源码可以找到bytecodeInterpreter.cpp

image.png

image.png

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

锁是怎么存储的

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

image.png

锁定义
image.png

基础锁对象定义:轻量级锁用到
image.png

对象头(存储锁标记)定义:偏向锁,轻量级锁(栈锁),重量级锁都会用到
image.png

对象头存储:包含2位锁标记

image.png

监视器ObjectMonitor(重量级锁使用)

image.png

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

image.png

快速加锁阶段:偏向锁

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

image.png

image.png

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

image.png

image.png

image.png

image.png

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

image.png

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

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

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

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

截屏2024-07-22 22.50.01.png

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

截屏2024-08-15 08.05.50.png

截屏2024-07-23 21.12.31.png

看到到这里,轻量级锁加锁流程已经完成,首先复制一份加锁对象对象头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 ;
  }
}
AI 代码解读

image.png

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

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

1、重入锁或者当前线程加锁的情况

image.png

2、自旋锁

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

image.png

首先尝试固定次数自旋

image.png

接着尝试自适应自旋

image.png

自旋锁,包含固定次数自旋自适应次数自旋, 自适应自旋会根据自旋成功失败增加或者减少下次自旋次数

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

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

首先修改线程状态为阻塞状态。
image.png

接着再尝试自旋加锁,如果自旋加锁失败,线程park阻塞
image.png

升级为重量级锁后,加锁失败的线程进入阻塞状态,并进入等待队列等待被唤醒。

image.png

升级为重量级锁后,对象头的分布情况:

image.png

三、总结

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

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

偏向锁存储在加锁目标对象的对象头上。分布存储加偏向锁成功的线程id和锁标记。

image.png

轻量级锁存储在加锁线程独有的BasicLock对象,以及加锁目标对象的对象头markword

截屏2024-07-23 21.12.31.png

自旋锁和重量级锁存储在ObjectMonitor对象,等待锁的线程存储在ObjectMonitor的队列上,锁标记同样存储在加锁对象的对象头上

image.png

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

通过加锁对象头markword可以知道对象当前加了哪种锁。

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

我们致力于分享互联网领域的各种技术干货,欢迎关注我们一起交流一起学习哦!

image.png

目录
打赏
0
3
3
0
42
分享
相关文章
美团面试:说说OOM三大场景和解决方案? (绝对史上最全)
小伙伴们,有没有遇到过程序突然崩溃,然后抛出一个OutOfMemoryError的异常?这就是我们俗称的OOM,也就是内存溢出 本文来带大家学习Java OOM的三大经典场景以及解决方案,保证让你有所收获!
4347 0
美团面试:说说OOM三大场景和解决方案? (绝对史上最全)
AQS深度解析与技术模拟
【11月更文挑战第26天】AbstractQueuedSynchronizer(AQS)是Java并发包(java.util.concurrent)中的一个核心组件,为构建锁和其他同步器提供了一个强大的基础框架。AQS通过定义一套多线程访问共享资源的同步器框架,极大地简化了同步组件的开发。本文将通过第一原理对AQS进行深入分析,涵盖其相关概念、业务场景、历史背景、功能点、底层原理,并使用Java代码进行模拟,以帮助读者全面理解AQS。
163 1
探秘Redis读写策略:CacheAside、读写穿透、异步写入
本文介绍了 Redis 的三种高可用性读写模式:CacheAside、Read/Write Through 和 Write Behind Caching。CacheAside 简单易用,但可能引发数据不一致;Read/Write Through 保证数据一致性,但性能可能受限于数据库;Write Behind Caching 提高写入性能,但有数据丢失风险。开发者应根据业务需求选择合适模式。
1590 2
探秘Redis读写策略:CacheAside、读写穿透、异步写入
深入学习RocketMQ的底层存储设计原理
文章深入探讨了RocketMQ的底层存储设计原理,分析了其如何通过将数据和索引映射到内存、异步刷新磁盘以及消息内容的混合存储来实现高性能的读写操作,从而保证了RocketMQ作为一款低延迟消息队列的读写性能。
JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透
本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。
2024消息队列“四大天王”:Rabbit、Rocket、Kafka、Pulsar巅峰对决
本文对比了 RabbitMQ、RocketMQ、Kafka 和 Pulsar 四种消息队列系统,涵盖架构、性能、可用性和适用场景。RabbitMQ 以灵活路由和可靠性著称;RocketMQ 支持高可用和顺序消息;Kafka 专为高吞吐量和低延迟设计;Pulsar 提供多租户支持和高可扩展性。性能方面,吞吐量从高到低依次为
1404 1
探索ConcurrentHashMap:从底层到应用的深度剖析
【10月更文挑战第6天】在Java并发编程中,ConcurrentHashMap是一个非常重要的数据结构,它提供了一种线程安全的哈希表实现。本文将深入探讨ConcurrentHashMap的底层存储结构、红黑树转换时机、数组扩容时机、核心属性sizeCtl、数组初始化、DCL操作、散列算法、写入操作的并发安全、计数器的安全机制以及size方法的实现策略。
161 1
大厂面试高频:Kafka、RocketMQ、RabbitMQ 的优劣势比较
本文深入探讨了消息队列的核心概念、应用场景及Kafka、RocketMQ、RabbitMQ的优劣势比较,大厂面试高频,必知必会,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:Kafka、RocketMQ、RabbitMQ 的优劣势比较
(三)死磕并发之深入Hotspot源码剖析Synchronized关键字实现
关于源码分析如果不是功底特别深厚的小伙伴可能需要用心的去细心咀嚼,千万不要抱着看一遍就能懂的心态学习,不然最终也没有任何作用。
130 5
【RocketMQ系列八】SpringBoot集成RocketMQ-实现普通消息和事务消息
【RocketMQ系列八】SpringBoot集成RocketMQ-实现普通消息和事务消息
763 1
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问