从 JVM 源码(HotSpot)看 synchronized 原理

news2025/3/6 20:46:24

大家好,我是此林。

不知道大家有没有这样一种感觉,网上对于一些 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. 重量级锁开销大的原因?

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入探索Python机器学习算法:模型调优

深入探索Python机器学习算法:模型调优 文章目录 深入探索Python机器学习算法:模型调优模型调优1. 超参数搜索方法1.1 网格搜索(Grid Search)1.2 随机搜索(Random Search)1.3 贝叶斯优化(Bayesia…

postman请求后端接受List集合对象

后端集合 post请求,即前端请求方式

Kimi“撞车”DeepSeek!新一代注意力机制的极限突破!

近期,各方大佬在注意力机制上又“打起来了”。首先登场的是顶流DeepSeek,新论文梁文锋署名,提出了一种新的注意力机制NSA。同天,Kimi杨植麟署名的新注意力架构MoBA开源。紧接着,华为诺亚提出高效选择注意力架构ESA。 …

计算机网络---SYN Blood(洪泛攻击)

文章目录 三次握手过程SYN Flood攻击原理防御措施协议层优化网络层拦截系统配置调整 TCP协议是 TCP/IP 协议栈中一个重要的协议,平时我们使用的浏览器,APP等大多使用 TCP 协议通讯的,可见 TCP 协议在网络中扮演的角色是多么的重要。 TCP 协议…

Ollama存在安全风险的情况通报及解决方案

据清华大学网络空间测绘联合研究中心分析,开源跨平台大模型工具Ollama默认配置存在未授权访问与模型窃取等安全隐患。鉴于目前DeepSeek等大模型的研究部署和应用非常广泛,多数用户使用Ollama私有化部署且未修改默认配置,存在数据泄露、算力盗…

健康医疗大数据——医疗影像

一、 项目概述 1.1 项目概述 1.2 项目框架 1.3 项目环境 1.4 项目需求 二、项目调试与运行 2.1需求分析 2.2具体实现 三、项目总结 项目概述 项目概述 本项目旨在应用大数据技术于医疗影像领域,通过实训培养团队成员对医疗大数据处理和分析的实际…

学生管理信息系统的需求分析与设计

伴随教育的迅猛演进以及学生规模的不断扩增,学生管理信息系统已然成为学校管理的关键利器。此系统能够助力学校管控学生的课程成绩、考勤记载、个人资讯等诸多数据,提升学校的管理效能与服务品质。 一.需求分析 1.1 学生信息管理 学生信息在学校管理体…

基于微信小程序的停车场管理系统的设计与实现

第1章 绪论 1.1 课题背景 随着移动互联形式的不断发展,各行各业都在摸索移动互联对本行业的改变,不断的尝试开发出适合于本行业或者本公司的APP。但是这样一来用户的手机上就需要安装各种软件,但是APP作为一个只为某个公司服务的一个软件&a…

前端小案例——520表白信封

前言:我们在学习完了HTML和CSS之后,就会想着使用这两个东西去做一些小案例,不过又没有什么好的案例让我们去练手,本篇文章就提供里一个案例——520表白信封 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨想要了解更多内容可以访问我的主…

【最后203篇系列】010 关于矩阵的一点思考

说明 今天拿起一本矩阵的书又翻了翻,毕竟AI搞到最后还得是数学。 我是感觉自己高数始终有点学的迷迷糊糊的,就打算这一年慢慢把矩阵部分扫一遍,毕竟这快肯定是实打实有用的。其他高级部分就等我发财之后再说了,哈哈。 内容 今…

Python快捷手册

Python快捷手册 后续会陆续更新Python对应的依赖或者工具使用方法 文章目录 Python快捷手册[toc]1-依赖1-词云小工具2-图片添加文字3-BeautifulSoup网络爬虫4-Tkinter界面绘制5-PDF转Word 2-开发1-多线程和队列 3-运维1-Requirement依赖2-波尔实验室3-Anaconda3使用教程4-CentO…

DeepSeek崛起:如何在云端快速部署你的专属AI助手

在2025年春节的科技盛宴上,DeepSeek因其在AI领域的卓越表现成为焦点,其开源的推理模型DeepSeek-R1擅长处理多种复杂任务,支持多语言处理,并通过搜索引擎获取实时信息。DeepSeek因其先进的自然语言处理技术、广泛的知识库和高性价比…

Linux的用户与权限--第二天

认知root用户(超级管理员) root用户用于最大的系统操作权限 普通用户的权限,一般在HOME目录内部不受限制 su与exit命令 su命令: su [-] 用户名 -符号是可选的,表示切换用户后加载环境变量 参数为用户名&#xff0c…

Zookeeper 及 基于ZooKeeper实现的分布式锁

1 ZooKeeper 1.1 ZooKeeper 介绍 ZooKeeper是一个开源的分布式协调服务,它的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。 原语:操作系统或…

Ubuntu20.04双系统安装及软件安装(五):VSCode

Ubuntu20.04双系统安装及软件安装(五):VSCode 打开VScode官网,点击中间左侧的deb文件下载: 系统会弹出下载框,确定即可。 在文件夹的**“下载”目录**,可看到下载的安装包,在该目录下…

【计算机网络入门】初学计算机网络(十一)重要

目录 1. CIDR无分类编址 1.1 CIDR的子网划分 1.1.1 定长子网划分 1.1.2 变长子网划分 2. 路由聚合 2.1 最长前缀匹配原则 3. 网络地址转换NAT 3.1 端口号 3.2 IP地址不够用? 3.3 公网IP和内网IP 3.4 NAT作用 4. ARP协议 4.1 如何利用IP地址找到MAC地址…

经验分享:用一张表解决并发冲突!数据库事务锁的核心实现逻辑

背景 对于一些内部使用的管理系统来说,可能没有引入Redis,又想基于现有的基础设施处理并发问题,而数据库是每个应用都避不开的基础设施之一,因此分享个我曾经维护过的一个系统中,使用数据库表来实现事务锁的方式。 之…

C++-第二十章:智能指针

目录 第一节:std::auto_ptr 第二节:std::unique_ptr 第三节:std::shared_ptr 第四节:std::shared_ptr的缺陷 4-1.循环引用 4-2.删除器 下期预告: 智能指针的作用是防止指针出作用域时忘记释放内存而造成内存泄漏&…

chrome Vue.js devtools 提示不支持该扩展组件,移除

可能是版本不兼容,可以重新安装,推荐网址极简插件官网_Chrome插件下载_Chrome浏览器应用商店 直接搜索vue,下载旧版,vue2、vue3都支持,上面那个最新版本试了下,vue2的肯定是不能用