【Java】synchronized关键字详解:从字节码到对象头与锁升级

【Java】synchronized关键字详解:从字节码到对象头与锁升级



👨‍💻程序员三明治个人主页
🔥 个人专栏: 《设计模式精解》《重学数据结构》

🤞先做到 再看见!


目录

synchronized底层原理(总结版)

synchronize底层使用的是minitor,Monitor 被翻译为监视器,是由jvm提供,c++语言实现。

使用javap -v xxx.class反编译一段代码可以看到机器指令

  • monitorenter 上锁开始的地方
  • monitorexit 解锁的地方
  • 其中被monitorenter和monitorexit包围住的指令就是上锁的代码
  • 第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁

monitor主要就是跟这个对象产生关联,如下图

Monitor内部具体的存储结构:

  • Owner:存储当前获取锁的线程,只能有一个线程可以获取
  • EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
  • WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程

具体的流程:

  • 进入synchorized代码块时,先让lock(对象锁)关联monitor,然后判断Owner是否有线程持有
  • 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功
  • 如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)
  • 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待

参考回答:

  • Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
  • 它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor
  • 在monitor内部有三个属性,分别是owner、entrylist、waitset
  • 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程好的,我们来详细、深入地剖析一下 synchronized 在 JVM 中的底层实现原理。这对于理解 Java 并发编程至关重要。

synchronized 底层原理(详解版)

synchronized 的底层原理可以从三个层面来看:字节码层面JVM 底层实现硬件层面。我们逐层深入。

1. 字节码层面:monitorenter 和 monitorexit

当我们使用 synchronized 关键字时,无论是修饰代码块还是方法,在编译后的字节码中都会生成对应的指令。

  • 同步代码块
    对于 synchronized(object) { ... },编译器会在同步代码块的前后分别生成 monitorentermonitorexit 指令。
publicvoidmethod(){synchronized(obj){// 同步代码块System.out.println("hello");}}

编译后的字节码大致如下:

public void method(); Code: 0: aload_0 1: getfield #2 // 获取对象引用 obj 4: dup 5: astore_1 6: monitorenter // 进入同步块,尝试获取锁 7: getstatic #3 // 获取 System.out 10: ldc #4 // 加载 "hello" 12: invokevirtual #5 // 调用 println 15: aload_1 16: monitorexit // 正常退出同步块,释放锁 17: goto 25 20: astore_2 21: aload_1 22: monitorexit // 异常退出同步块,释放锁 (确保在异常情况下也能释放锁) 23: aload_2 24: athrow 25: return 

关键点

- 可以看到有两个 `monitorexit` 指令,第一个用于正常退出,第二个用于处理异常情况(隐藏在 `finally` 语义中),这确保了即使同步块内抛出异常,锁也能被正确释放。 
  • 同步方法
    对于 synchronized 修饰的方法,方法常量池中会设置 ACC_SYNCHRONIZED 标志。
publicsynchronizedvoidmethod(){// 方法体}
- 当方法调用时,调用指令(如 `invokevirtual`)会检查这个标志。如果设置了,执行线程会先尝试获取锁(对于实例方法是 `this`,对于静态方法是该类的 `Class` 对象),再执行方法体。方法执行完毕后,无论是正常返回还是异常抛出,都会自动释放锁。 

小结:从字节码看,synchronized 的实现依赖于 monitorentermonitorexit 这一对指令,或者方法的 ACC_SYNCHRONIZED 标志。

2. JVM 底层实现:对象头与 Monitor

monitorentermonitorexit 指令背后的具体实现,是 JVM 的核心。其关键在于 Java 对象头Monitor

2.1 Java 对象头(Mark Word)

在 HotSpot 虚拟机中,每个 Java 对象在内存中存储的布局分为三部分:对象头、实例数据、对齐填充

其中,对象头 是理解锁的关键。它包含两部分信息:

  1. Mark Word:存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID 等。它是实现锁的“主战场”。
  2. Klass Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

为了在极小的空间内存储尽可能多的信息,Mark Word 被设计成一个非固定的动态数据结构。它会根据对象的状态复用自己的存储空间。下图清晰地展示了 32 位 JVM 下 Mark Word 在不同状态下的结构:

(在 64 位 JVM 下,结构类似,只是空间更大。)

关键点:注意最后 2 位(lock),它标识了对象的锁状态。锁的升级过程就体现在这 2 位的变化上。

2.2 Monitor(管程/监视器锁)

JVM 为每个对象都关联了一个内置的 Monitor(管程)。monitorenter 指令的本质就是尝试去获取这个对象对应的 Monitor。

一个 Monitor 由以下部分组成:

  • Owner:当前持有该 Monitor 的线程。初始为 null
  • EntryList:处于 Blocked 状态的、等待锁的线程队列。当 Owner 释放锁时,JVM 会从 EntryList 中挑选一个线程来成为新的 Owner。
  • WaitSet:处于 Waiting 状态的、调用了 Object.wait() 方法的线程队列。这些线程在等待其他线程的通知(notify/notifyAll)。

工作流程

  1. 当线程执行到 monitorenter 指令时,会尝试进入(enter)该对象的 Monitor。
  2. 如果 Monitor 的 Owner 为 null,则该线程成功成为 Owner,并将锁的计数器 +1。
  3. 如果该线程已经是 Owner(可重入锁),它再次进入,锁计数器再次 +1。
  4. 如果 Owner 是其他线程,则当前线程会进入 EntryList,进入 BLOCKED 状态,直到 Owner 线程释放锁。
  5. 当线程执行 monitorexit 指令时,锁计数器 -1。当计数器减到 0 时,线程释放 Monitor,不再担任 Owner。然后,EntryList 中的线程会开始竞争锁。
3. 锁的升级与优化

在 Java 6 之前,synchronized 是一个重量级锁,性能较差,因为它依赖于操作系统的 Mutex Lock(互斥锁),需要进行用户态到内核态的切换,耗时较长。

为了减少这种性能开销,Java 6 引入了锁升级机制。synchronized 的锁状态从低到高分为四种,升级路径是单向的:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

3.1 偏向锁
  • 目的:在没有竞争的情况下,消除整个同步操作。假设在大多数情况下,锁不仅不存在竞争,而且总是由同一线程多次获得。
  • 原理
    • 当一个线程访问同步块时,会在对象头和栈帧中的锁记录里存储偏向的线程 ID
    • 以后该线程再次进入和退出同步块时,不需要进行 CAS 操作来加锁和解锁,只需简单测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。
    • 如果测试成功,表示线程已经获得了锁。
  • 撤销:一旦出现另一个线程来尝试竞争锁,偏向模式就宣告结束。持有偏向锁的线程会被挂起,JVM 会撤销偏向锁,然后升级为轻量级锁。
注意:在 Java 15 之后,偏向锁被标记为废弃并默认禁用,因为维护其带来的收益已不如从前。但理解其原理依然重要。
3.2 轻量级锁
  • 目的:在竞争不激烈(“近交替执行”)的情况下,避免直接使用重量级锁带来的性能消耗。
  • 加锁过程
    1. 在当前线程的栈帧中创建一个名为 锁记录 的空间。
    2. 将对象头的 Mark Word 复制到锁记录中(称为 Displaced Mark Word)。
    3. 线程尝试使用 CAS 操作将对象头的 Mark Word 替换为指向锁记录的指针。
      • 如果成功,当前线程获得锁。并将对象 Mark Word 的最后 2 位设置为 00,表示轻量级锁状态。
      • 如果失败,表示存在竞争(另一个线程也修改了 Mark Word)。
  • 解锁过程
    • 使用 CAS 操作将 Displaced Mark Word 替换回对象头。
    • 如果成功,则同步过程顺利完成。
    • 如果失败,说明锁已经升级,需要释放锁的同时唤醒被挂起的线程。
3.3 重量级锁
  • 触发条件:当轻量级锁竞争失败后,会自旋尝试获取锁一定次数(自旋锁)。如果自旋后依然失败,锁就会膨胀为重量级锁。
  • 特点
    • 此时 Mark Word 中存储的是指向重量级锁(Monitor)的指针。
    • 等待锁的线程都会进入 EntryList,进入 BLOCKED 状态。
    • 依赖于操作系统底层的 Mutex Lock,需要进行用户态到内核态的切换,成本最高。

4. 硬件层面:内存屏障与 CAS

synchronized 的语义保证了原子性、可见性和有序性

  • 可见性与有序性:是通过在编译器和处理器层面插入 内存屏障 来实现的。在同步块开始时加 Load Barrier,在同步块结束时加 Store Barrier,强制将工作内存中的修改刷新到主内存,并禁止指令重排序。
  • 原子性:对于简单的 monitorenter/monitorexit,由 Monitor 保证。对于锁升级过程中的状态变更(如轻量级锁的获取),则是通过 CAS 操作实现的。CAS 是一条 CPU 原子指令(cmpxchg),它保证了“比较-交换”操作的原子性。

JVM锁升级是什么?

Monitor实现的锁属于重量级锁,你了解过锁升级吗?

  • Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
  • 在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
对象的内存结构
MarkWord

我们可以通过lock的标识,来判断是哪一种锁的等级

  • 后三位是001表示无锁
  • 后三位是101表示偏向锁
  • 后两位是00表示轻量级锁
  • 后两位是10表示重量级锁
再说Monitor重量级锁

每个对象的markword都可以设置monoitor的指针,让对象与monitor产生关联

轻量级锁

**加锁的流程 **

1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。

2.通过CAS指令将Lock Record的地址存储在对象头的mark word中(数据进行交换),如果对象处于无锁状态代表该线程获得了轻量级锁。

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。

4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。

解锁过程

1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。

2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。

3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word 恢复成为无锁状态。如果失败则膨胀为重量级锁。

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有.

**加锁的流程 **

1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。

2.通过CAS指令将Lock Record的线程id存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,性能相对轻量级锁更好一些

解锁流程参考轻量级锁

如果我的内容对你有帮助,请辛苦动动您的手指为我点赞,评论,收藏。感谢大家!!

在这里插入图片描述


我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=h70g0sv71wz

Read more

【嵌入式硬件】FPGA开发从入门到精通

【嵌入式硬件】FPGA开发从入门到精通

目录 一、FPGA 是什么 二、学习前的准备 (一)硬件准备 (二)软件准备 三、基础知识入门 (一)数字电路基础回顾 (二)Verilog HDL 语言基础 四、FPGA 开发流程实战 (一)创建工程 (二)编写代码 (三)综合与实现 (四)仿真验证 (五)下载与调试 五、学习资源推荐 (一)书籍 (二)在线课程 (三)论坛和博客 六、总结与展望 一、FPGA 是什么 FPGA,即现场可编程门阵列(Field-Programmable Gate Array) ,是一种可编程逻辑器件。

By Ne0inhk

一文吃透SBUS协议:从原理到实战(无人机/航模/机器人适用)

在无人机、航模、机器人等精密控制领域,“稳定、快速、可靠”是控制信号传输的核心诉求。传统的PWM信号虽然简单直观,但存在通道数有限、抗干扰能力弱、布线复杂等痛点。而SBUS(Serial Bus)协议——由FUTABA公司专为遥控设备设计的串行数字通信协议,凭借单线传输多通道数据、抗干扰强、延迟低的核心优势,逐渐成为行业主流。 本文将从“是什么-怎么工作-协议细节-厂家产品-接口设计-代码实现-实战技巧-常见问题”八个维度,用最通俗的语言+大量对比表格,全面拆解SBUS协议。无论你是刚入门的电子爱好者,还是需要落地项目的工程师,都能从本文中找到所需的实用信息。 一、SBUS协议基础认知:核心定位与优势对比 在深入技术细节前,我们先通过对比和基础定义,快速建立对SBUS的认知。很多人会把SBUS和常见的UART、PWM等混淆,这里先明确其核心定位:SBUS是基于反向电平UART的“应用层控制协议”,专门用于遥控器与接收机、接收机与飞控/执行器之间的控制信号传输。 1.1 为什么需要SBUS?传统方案的痛点 在SBUS出现之前,航模和早期无人机主要使用PWM或PPM协议传输控

By Ne0inhk
从人类视频到机器人跳舞:BeyondMimic 全流程解析与 rl_sar 部署实践

从人类视频到机器人跳舞:BeyondMimic 全流程解析与 rl_sar 部署实践

0. 前言 让人形机器人学会跳舞,听起来像是科幻电影中的场景,但在强化学习和运动模仿技术的推动下,这件事正在变得越来越现实。本文将完整介绍一条从"人类 RGB 视频"到"真实机器人跳舞"的技术链路:首先通过视觉算法从视频中提取人体运动轨迹,然后将人体模型重定向到机器人关节空间,接着在仿真环境中进行强化学习训练,最后在 MuJoCo 中验证并部署到真实的 Unitree G1 人形机器人上。 整条流程涉及四个核心开源项目:GVHMR(视频到人体模型)、GMR(人体到机器人重定向)、BeyondMimic(强化学习训练框架)、以及 rl_sar(仿真验证与真机部署框架)。本文不仅会逐一拆解每个环节的原理和操作步骤,还会深入分析 BeyondMimic 的算法设计,并详细记录将训练产物迁移到 rl_sar 项目中进行 sim2sim 和 sim2real 部署时遇到的关键问题与解决方案。 下图展示了

By Ne0inhk

宇树机器人g1二次开发:建图,定位,导航手把手教程(二)建图部分:开始一直到打开rviz教程

注意: 本教程为ros1,需要ubuntu20.04,使用算法为fase_lio 本教程为遵循的网上开源项目:https://github.com/deepglint/FAST_LIO_LOCALIZATION_HUMANOID.git 一、系统环境准备 1.1. 安装必要的依赖库 # 安装C++标准库 sudo apt install libc++-dev libc++abi-dev # 安装Eigen3线性代数库 sudo apt-get install libeigen3-dev 库说明: * libc++-dev:C++标准库开发文件 * libeigen3-dev:线性代数库,用于矩阵运算和几何变换 * 这些是编译FAST-LIO和Open3D必需的数学和系统库 二、创建工作空间和准备 2.1. 创建定位工作空间 mkdir

By Ne0inhk