从 JVM 源码(HotSpot)看 synchronized 原理

大家好,我是此林。

不知道大家有没有这样一种感觉,网上对于一些 Java 框架和类的原理实现众说纷纭,看了总是不明白、不透彻。常常会想:真的是这样吗?

今天我们就从 HotSpot 源码级别去看 synchronized 的实现原理。全文以问题-解答的模式来展开讲述,方便大家理解。

1. 修饰代码块和修饰方法在字节码层面有什么不同?

synchronized 关键字可以修饰在三个地方:代码块、实例方法、静态方法。

但 synchronized 本质上是作用在对象上。

修饰在代码块:作用于括号里的对象

修饰在实例方法:作用于当前 this 实例对象

修饰在静态方法:作用于当前 Class 对象

1.1. 修饰在代码块

public class A {
    public static void main(String[] args) {

    }

    public void test() {
        synchronized (this) {
            System.out.println("test");
        }
    }
}

上面这段代码用 IDEA 中的 jclasslib 插件反编译看下字节码。

执行 monitorenter 代表去抢占 monitor 对象,抢到了 monitor 对象就代表持有了锁。

monitorexit 也就很好理解了,是释放锁的意思。

为什么 monitorexit 要执行两次呢?

因为代码如果出现异常了,也需要解锁,否则就死锁了。

从字节码的角度,我们也就可以知道为什么 synchronized 不需要手动解锁了。

因为编译器生成的字节码里已经给我们考虑好了,异常情况也考虑到了。

1.2. 修饰在方法上

public class A {
    public static void main(String[] args) {

    }

    public synchronized void test() {
        System.out.println("test");
    }
}

同样的,这段代码我们再反编译一下。 

不过,这一次好像没有自动加 monitorenter 和 monitorexit 指令啊。

别急,你看看当前方法的访问标志。这里是 public synchronized 。

这样 JVM 就知道这个方法是被 synchronized 标记的,在进入方法前后会进行加锁解锁操作。

对比一下之前修饰代码块的访问标志。

所以 synchronized 修饰代码块和修饰方法在字节码层面是不一样的,修饰代码块会自动加上 monitorenter 和 monitorexit 指令,修饰方法时会在方法的访问标志上做标记。 

2. Java 对象结构是怎么样的?

下面给一张图,对 Java 对象布局有个直观的了解。

上图可知,Java 对象结构分为 对象头、实例数据、对齐填充。

在 HotSpot 源码里,Java 对象结构的代码在 src\share\vm\oops 里,instanceOop、instanceKlass、oop 几个C++的文件描述了对象的定义(有兴趣的小伙伴可以自行去研究)。

笔者用的 openjdk 8。

而对象头又分为:MarkWord、Klass Pointer(类型指针)、数组长度(只有数组有)。

我们现在关注锁,所以重点放在 MarkWord 上,各种锁操作都和 MarkWord 有强关联。下面是 MarkWord 的内部结构。

从图中可以看到,当为重量级锁的时候,对象头的锁标志位为 10 ,并且会有一个指针指向这个 Monitor 对象。所以 java对象和 Monitor 就是这么关联上的。

疑点解答:每个对象都有一个 monitor 对象 (C++实现)和它关联。

其实不是这样的。

看上表可以知道,

当 synchronized 为偏向锁的时候,锁对象和线程ID关联

当 synchronized 为轻量级锁的时候,锁对象和lockRecord关联

当 synchronized 为重量级级锁的时候,锁对象和monitor对象关联

也就是说,只有当 synchronized 升级为重量级级锁的时候,锁对象的对象头的markword才会指向monitor对象。

3. synchronized 锁升级流程是怎么样的?

先说整体流程,无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。

下面这张表很重要!敲黑板!

锁类型用途场景
偏向锁只有一个线程来抢锁。如果后续又来了一个线程,那么偏向锁会被立即撤销,升级为轻量级锁。
轻量级锁有两个线程来抢锁,但这两个线程不会同时抢锁,交替执行。如果出现同时抢锁场景,轻量级锁会立即升级为重量级锁。
重量级锁多个线程同时来抢锁,也就是我们常说的互斥锁。

来,直接上 JVM 源码! 

3.1. 偏向锁

3.1.1. 偏向锁是什么?

偏向锁是什么呢?

只有一个线程的情况下,没有其他线程来竞争锁,所以频繁 CAS 会造成性能开销。

所以 JVM 开发者们弄出了偏向锁,就是偏向一个线程,下次这个线程来可以直接获取锁

再看下这张图。

举个例子:

比如有个 synchronized (obj){}

1. 时间点:9:00:00

    线程A来了,通过 CAS 把obj锁对象的对象头的 markword 指向线程A的ID。

2. 时间点:9:00:05

    线程A又来了,发现obj锁对象的 markword 指向线程A的ID,那么线程A直接放行,无需再次 CAS ,相当于无锁的性能。

3. 时间点:9:00:10

    线程B来了,那么偏向锁直接撤销,升级为轻量级锁。

(注:如果在 时间点:9:00:00 - 9:00:05 之间,线程B来了,那么偏向锁也会直接撤销,升级为轻量级锁

对象头里会记录持有偏向锁的线程id,并把最后三个比特位设置为 101,第一个1代表是偏向锁。 

之后有线程请求获取这把锁,只需要判断对象头的 markword 的后三位是不是 101,线程ID是否和当前线程相等。

3.1.2. 如何开启偏向锁?

这个就是 JVM 参数调优了。

可以通过参数 -XX:+UseBiasedLocking 来开启。

可以通过参数 -XX:-UseBiasedLocking 来关闭。

在高并发应用中,建议关闭偏向锁;在低并发应用中,可以考虑开启偏向锁。

3.1.3. 为什么在在高并发应用中,建议关闭偏向锁?

偏向锁只适合一个线程抢锁的场景。在只有一个线程的场景下,只需要第一次 CAS 把对象头的markword 指向当前线程ID,后续只需要比对线程ID,无需重复 CAS,实现几乎无锁的性能。

但是一旦有其他线程来抢锁,偏向锁会立刻撤销,而撤销会消耗大量的资源。

具体来说,偏向锁的撤销需要等待全局安全点(safepoint),需要 STW(Stop The World), 遍历所有线程栈,检查偏向线程是否还存活并且持有锁。如果偏向线程存活且持有锁,升级为轻量级锁。

上源码(偏向锁升级为轻量级锁)。

之前也说过了,轻量级锁时,锁对象的对象头的 markword 指向 lockRecord(BasicObjectLock)对象。

所以说,不同级别锁的本质是靠锁对象头的markword来区分关联的。

3.1.4. 代码执行完了,偏向锁会释放吗?

先说答案,不会。

在 HotSpot 虚拟机中,偏向锁的释放并不是在代码执行完(同步块退出)时立即触发的。偏向锁的设计目标是 无竞争场景下的性能优化,因此即使线程退出同步块,只要没有其他线程竞争,对象头仍会保持偏向模式,偏向锁不会主动释放。

那偏向锁的释放(撤销)触发时机呢?
当其他线程尝试获取已被偏向的锁时,JVM 会触发偏向锁的撤销(Revoke Bias),将对象头升级为轻量级锁。

3.1.5. 偏向锁有什么优化吗?

偏向锁在有竞争的时候是要执行撤销操作的,其实就是要升级成轻量级锁。

而当一个对象撤销的次数过多,经常被撤销,次数到了一定阈值(XX:BiasedLockingBulkRebiasThreshold,默认为 20 ) 就会把当代的偏向锁废弃,把 Klass 对象 的 epoch 加一。

看见了对象头的markword还有个 Epoch 吧? 

所以当 Klass对象和 实例锁对象的 epoch 值不等的时候,当前线程可以将该锁重偏向至自己,因为前一代偏向锁已经废弃了。

当撤销次数超过另一个阈值(XX:BiasedLockingBulkRevokeThreshold,默认值为 40),则废弃此类的偏向功能,也就是说这个类都无法偏向了(永久废弃)。

3.2. 轻量级锁

 3.2.1. 轻量级锁是什么?

还记得我们之前说过的这个表格吗?

锁类型用途场景
偏向锁只有一个线程来抢锁。如果后续又来了一个线程,那么偏向锁会被立即撤销,升级为轻量级锁。
轻量级锁有两个线程来抢锁,但这两个线程不会同时抢锁,交替执行。如果出现同时抢锁场景,轻量级锁会立即升级为重量级锁。
重量级锁多个线程同时来抢锁,也就是我们常说的互斥锁。

轻量级锁应用场景多个线程都是在不同的时间段来请求同一把锁,此时根本就用不需要阻塞线程,连 monitor 对象都不需要,所以就引入了轻量级锁这个概念,避免了系统调用,减少了开销。

3.2.2. 轻量级锁时,对象头的markword指向lockRecord?

前面我们说过,轻量级锁时,锁对象的对象头的markword指向lockRecord。

那这个lockRecord又是什么?

lockRecord 本质上就是 BasicObjectLock 对象,不过它不是分配在堆上的,是分配在线程栈上的,也就是线程私有,每个线程都有自己的 BasicObjectLock对象。

看到这里,再问一句:那重量级锁的 monitor 对象呢?

monitor 对象本质上是一个 C++ 实现的 ObjectMonitor 对象,它分配在堆上,全局唯一,所有线程共享。因为它全局唯一共享,所以 ObjectMonitor 会有个 owner 字段,用来标识当前哪个线程占有了 monitor。

3.2.3. 说说轻量级锁的加锁流程?

看下图源码吧!

其实本质上就是通过 CAS 把锁对象对象头的markword指向当前线程栈上私有的BasicLock。

3.2.4. 那轻量级锁的可重入逻辑怎么实现的?

前面已经说过了轻量级锁的加锁逻辑,如果无锁,直接把锁对象对象头的markword指向当前线程栈上私有的BasicLock。

如果已经有锁,先断言判断一下 markword 的 BasicLock 和当前线程的BasicLock是否相等,如果相等,那么就执行可重入逻辑。

下面一张图应该很清晰了。

可以看到,每个 lockRecord 里拷贝了锁对象的markword,

加锁流程如下:

1. 每次加锁时,线程栈都会入栈一个 lockRecord。

2. 先检查锁对象的 markword 是否已经指向了 lockRecord,如果没有,说明第一次加锁,lockRecord 拷贝一份 原始无锁态的markword的副本 到字段_displaced_header,并且通过 CAS 让 markword 指向这个 lockRecord。

3. 如果锁对象的 markword 已经指向了 lockRecord 了,并且发现这个 lockRecord 属于当前线程栈,lockRecord 里的字段 _displaced_header 设置为 NULL。

解锁流程如下:

1. 解锁时,若发现 _displaced_header 为 NULL,说明是重入的,直接 return 返回,lockRecord 弹栈。

2. 若发现 _displaced_header 不为 NULL,那就 CAS 把现在markword 换成 原始无锁态的markword,这也就是为什么 lockRecord 要拷贝一份markword副本的原因

来看 JVM 轻量级锁解锁代码。

3.3. 重量级锁

3.3.1. 重量级锁是什么?

前面已经说过,重量级锁本质上就是锁对象头的markword指向一个堆空间上分配的、全局唯一的 ObjectMonitor 对象,这个 ObjectMonitor 对象有个属性 owner(标识哪个线程持有锁),recursions(锁重入次数),object(锁对象)。

至于 _WaitSet、_cxq、_EntryList 三个列表,_cxq 和 _EntryList 用于存放竞争锁失败被 park() 阻塞的线程。_WaitSet 里是存储已经获取到锁的线程,但是主动调用 wait() 的线程。

LockSupport.park()Thread.sleep()Object.wait()
是否释放锁不会释放锁不会释放锁

会释放锁,

无论重入几次(线程必须持有锁才能调用)

阻塞方式

无限期阻塞,

直到 unpark()

休眠到固定时间,

或 interrupt()

无限期阻塞,

进入 waitSet,

直到 notify() 或 notifyAll()

interrupt() 时不会抛异常,但 Thread.interrupted() 变 true会抛 InterruptedException异常会抛 InterruptedException异常
使用场景线程池线程挂起定时任务,休眠生产者-消费者,线程通信

3.3.2. 重量级锁加锁流程?

下面贴一张之前说的轻量级锁加锁流程:

在这之后,slow_enter() 方法最后,如果轻量级锁加锁失败,则 inflate,直接升级为重量级锁。

可以看到,轻量级锁加锁失败,是直接升级为重量级锁的(锁对象头markword指向ObjectMonitor 对象),并没有先进行自旋操作。 

至于说自旋优化,那也是在升级为重量级锁之后的操作。inflate方法是升级为重量级锁,enter方法是抢锁逻辑。来看enter方法。

好,下面重点来了,如果抢锁失败了呢? 

 如果 Knob_SpinEarly 开启(默认为1,开启),先 TrySpin() 自适应自旋 一波。

自适应自旋可以理解为多次CAS,它会通过一系列算法按之前的经验 动态调整 等待时间,次数等。

重点看 EnterI() 方法。

所以总的流程如下:

先再尝试一下获取锁,不行的话就自适应自旋,还不行就包装成 ObjectWaiter 对象加入到 _cxq 这个单向链表之中,挣扎一下还是没抢到锁的话,那么就要阻塞了,所以下面还有阻塞逻辑。

至此,重量级锁的加锁逻辑到此结束了。总结一下,偷个懒,贴一张别人的图。

3.3.3. 重量级锁的解锁流程?

解锁流程在 exit() 方法里:

recursions 减到0的时候,还会唤醒其他线程,这里有几种模式。

1. Qmode == 2

2. Qmode == 3

3. Qmode == 4

总结一下,网图,侵删。

3.3.4. 说说 wait() 和 notify() 方法?

再看下之前的表格:

LockSupport.park()Thread.sleep()Object.wait()
是否释放锁不会释放锁不会释放锁

会释放锁,

无论重入几次(线程必须持有锁才能调用)

阻塞方式

无限期阻塞,

直到 unpark()

休眠到固定时间,

或 interrupt()

无限期阻塞,

进入 waitSet,

直到 notify() 或 notifyAll()

interrupt() 时不会抛异常,但 Thread.interrupted() 变 true会抛 InterruptedException异常会抛 InterruptedException异常
使用场景线程池线程挂起定时任务,休眠生产者-消费者,线程通信

线程必须持有 synchronized 锁才能调用 wait() 方法。

wait() 逻辑很简单,就是将当前线程加入到 _waitSet 这个双向链表中,然后再执行 ObjectMonitor::exit 方法来释放锁。

notify() 逻辑也不难,就是从 _waitSet 头部拿节点,然后根据策略选择是放在 cxq 还是 EntryList 的头部或者尾部,并且进行唤醒。

现在再来看下这个图,应该心里很有数了。

3.3.5. 为什么会有_cxq 和 _EntryList 两个列表来放线程?

因为会有多个线程会同时竞争锁,竞争失败了先存在 _cxq 这个单向链表,在每次唤醒的时候搬迁一些线程节点到_EntryList 这个双向链表,降低 _cxq 的头部入队竞争。

3.3.6. 重量级锁开销大的原因?

阻塞和唤醒依赖于底层的操作系统实现,系统调用存在用户态与内核态之间的切换,所以有较高的开销,因此称之为重量级锁。

所以又引入了自适应自旋机制,来提高锁的性能。

我是此林,关注我吧!带你看不一样的世界!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/981372.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DeepSeek搭配Excel,制作自定义按钮,实现办公自动化!

今天跟大家分享下我们如何将DeepSeek生成的VBA代码,做成按钮,将其永久保存在我们的Excel表格中,下次遇到类似的问题,直接在Excel中点击按钮,就能10秒搞定,操作也非常的简单. 一、代码准备 代码可以直接询问…

Metal学习笔记十一:贴图和材质

在上一章中,您设置了一个简单的 Phong 光照模型。近年来,研究人员在基于物理的渲染 (PBR) 方面取得了长足的进步。PBR 尝试准确表示真实世界的着色,真实世界中离开表面的光量小于表面接收的光量。在现实世界中&#xf…

zabbix“专家坐诊”第277期问答

在线答疑:乐维社区 问题一 Q:这个怎么解决呢? A:缺少这个依赖。 Q:就一直装不上。 A:装 zabbix-agent2-7.0.0-releasel.el7.x86 64 需要前面提示的那个依赖才可以装。 问题二 Q:大佬,如果agen…

让单链表不再云里雾里

一日不见,如三月兮!接下来与我一起创建单链表吧! 目录 单链表的结构: 创建单链表: 增加结点: 插入结点: 删除结点: 打印单链表: 单链表查找: 单链表…

图像生成-ICCV2019-SinGAN: Learning a Generative Model from a Single Natural Image

图像生成-ICCV2019-SinGAN: Learning a Generative Model from a Single Natural Image 文章目录 图像生成-ICCV2019-SinGAN: Learning a Generative Model from a Single Natural Image主要创新点模型架构图生成器生成器源码 判别器判别器源码 损失函数需要源码讲解的私信我 S…

指纹细节提取(Matlab实现)

指纹细节提取概述指纹作为人体生物特征识别领域中应用最为广泛的特征之一,具有独特性、稳定性和便利性。指纹细节特征对于指纹识别的准确性和可靠性起着关键作用。指纹细节提取,即从指纹图像中精确地提取出能够表征指纹唯一性的关键特征点,是…

泵吸式激光可燃气体监测仪:快速精准守护燃气管网安全

在城市化进程加速的今天,燃气泄漏、地下管网老化等问题时刻威胁着城市安全。如何实现精准、高效的可燃气体监测,守护“城市生命线”,成为新型基础设施建设的核心课题。泵吸式激光可燃气体监测仪,以创新科技赋能安全监测&#xff0…

HTML label 标签使用

点击 <label> 标签通常会使与之关联的表单控件获得焦点或被激活。 通过正确使用 <label> 标签&#xff0c;可以使表单更加友好和易于使用&#xff0c;同时提高整体的可访问性。 基本用法 <label> 标签通过 for 属性与 id 为 username 的 <input> 元素…

数字万用表的使用教程

福禄克经济型数字万用表前面板按键功能介绍示意图 1. 万用表简单介绍 万用表是一种带有整流器的、可以测量交、直流电流、电压及电阻等多种电学参量的磁电式仪表。分为数字万用表&#xff0c;钳形万用表&#xff0c; &#xff08;1&#xff09;表笔分为红、黑二只。使用时黑色…

Python 爬取唐诗宋词三百首

你可以使用 requests 和 BeautifulSoup 来爬取《唐诗三百首》和《宋词三百首》的数据。以下是一个基本的 Python 爬虫示例&#xff0c;它从 中华诗词网 或类似的网站获取数据并保存为 JSON 文件。 import requests from bs4 import BeautifulSoup import json import time# 爬取…

2025年AI PPT工具精选:让演示文稿更智能、更高效

&#x1f4a1; 做PPT太难&#xff1f;没灵感&#xff1f;排版不好看&#xff1f;别怕&#xff0c;AI已经帮你安排好了&#xff01; 想知道2025年最值得推荐的AI PPT工具是哪款&#xff1f;答案就是——秒出PPT&#xff01;&#x1f680; 不仅能一键生成PPT&#xff0c;还能自…

qt-C++笔记之ubuntu22.04源码安装Qt6.8.2

qt-C笔记之ubuntu22.04源码安装Qt6.8.2 code review! 文章目录 qt-C笔记之ubuntu22.04源码安装Qt6.8.21.作者环境&#xff1a;ubuntu22.04、cmake202.安装3.关联已安装的 Qt6 到 Qt Creator4.附&#xff1a;ubuntu18.0的处理&#xff0c;可尝试&#xff0c;作者没有遇到这个问题…

单例模式(线程案例)

单例模式可以分为两种&#xff1a;1.饿汉模式 2.懒汉模式 一.饿汉模式 //饿汉模式&#x1f447; class MySingleTon{//因为这是一个静态成员变量&#xff0c;在类加载的时候&#xff0c;就创建了private static MySingleTon mySingleTon new MySingleTon();//创建一个静…

基于Matlab的多目标粒子群优化

在复杂系统的设计、决策与优化问题中&#xff0c;常常需要同时兼顾多个相互冲突的目标&#xff0c;多目标粒子群优化&#xff08;MOPSO&#xff09;算法应运而生&#xff0c;作为群体智能优化算法家族中的重要成员&#xff0c;它为解决此类棘手难题提供了高效且富有创新性的解决…

(2025年)工会考试该如何高效备考?有学习方法吗?

工会考试备考文章 工会考试高效备考指南 工会在维护职工权益、促进企业和谐发展中扮演着重要角色&#xff0c;工会考试则是选拔优秀工会工作者的关键途径。面对工会考试涉及的法律法规、组织管理以及维权服务等多方面知识&#xff0c;掌握科学备考方法是成功的关键。 法律法规是…

《机器学习数学基础》补充资料:向量范数

《机器学习数学基础》第1章1.5.3节介绍了向量范数的基本定义。 本文在上述基础上&#xff0c;介绍向量范数的有关性质。 注意&#xff1a; 以下均在欧几里得空间讨论&#xff0c;即欧氏范数。 1. 性质 实&#xff08;或复&#xff09;向量 x \pmb{x} x &#xff0c;范数 ∥…

Unity NGUI新手向几个问题记录

1.点Button没反应 制作Button组件时&#xff0c;不光要挂载Button脚本&#xff0c;还有挂载BoxCollider BoxCollider 接收事件 2.Button点击事件的增加与删除 使用.onClick.add增加事件&#xff0c;使用.onClick.Remove,.onClick.RemoveAt,onClick.RemoveRang,onClick.Clear移…

servlet tomcat

在spring-mvc demo程序运行到DispatcherServlet的mvc处理 一文中&#xff0c;我们实践了浏览器输入一个请求&#xff0c;然后到SpringMvc的DispatcherServlet处理的整个流程. 设计上这些都是tomcat servlet的处理 那么究竟这是怎么到DispatcherServlet处理的&#xff0c;本文将…

UniApp 中封装 HTTP 请求与 Token 管理(附Demo)

目录 1. 基本知识2. Demo3. 拓展 1. 基本知识 从实战代码中学习&#xff0c;上述实战代码来源&#xff1a;芋道源码/yudao-mall-uniapp 该代码中&#xff0c;通过自定义 request 函数对 HTTP 请求进行了统一管理&#xff0c;并且结合了 Token 认证机制 请求封装原理&#xff…

【音视频】ffmpeg命令分类查询

一、ffmpeg命令分类查询 -version&#xff1a;显示版本 ffmpeg -version-buildconf&#xff1a;显示编译配置&#xff0c;这里指的是你编译好的ffmpeg的选项 ffmpeg -buildconf-formats:显示可用格式&#xff08;muxersdemuxers&#xff09;&#xff0c;复用器和解复用器&am…