从 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. 重量级锁开销大的原因?

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

下载b站视频音频

文章目录 方案一:jjdown如何使用 方案二:bilibili哔哩哔哩下载助手如何使用进入插件网站插件下载插件安装 使用插件下载视频音频:复制音频下载地址 方案三:bat命令下载单个音频下载单个视频下载单个音视频 方案一:jjdo…

快速在本地运行SpringBoot项目的流程介绍

目录 前言 一、环境配置 1.1Java环境 1.2Maven环境 1.3IntelliJ IDEA安装 1.4MySql安装 二、项目导入与启动的过程 2.1Maven镜像和本地仓库 2.1.2镜像配置 2.1.3配置本地仓库 2.2导入项目与启动 2.2.1加载Maven设置 2.2.2配置jdk与java版本 2.2.3创建数据库 2.2…

分类预测 | Matlab实现CPO-SVM冠豪猪算法优化支持向量机多特征分类预测

分类预测 | Matlab实现CPO-SVM冠豪猪算法优化支持向量机多特征分类预测 目录 分类预测 | Matlab实现CPO-SVM冠豪猪算法优化支持向量机多特征分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现CPO-SVM冠豪猪算法优化支持向量机多特征分类预测&#xff…

not support ClassForName

com.alibaba.fastjson2.JSONException: not support ClassForName : java.lang.String, you can config JSONReader.Feature.SupportClassForName 官方说明中提到默认关闭, 可通过配置开启 JSON.config(JSONReader.Feature.SupportClassForName);

(贪心 跳跃游戏)leetcode 55

题解思路&#xff1a;代码随想录--代码随想录本题题解 本题不考虑每个结点走几步只考虑范围 在nums[0]2&#xff0c;也就是在nums[1]和nums[2]找到最大范围&#xff08;for(int i0;i<cover;i)) nums[1]3,也就是在nums[2]和nums[4]这个区间范围找到最大范围&#xff0c;而因…

Unity中动态切换光照贴图LightProbe的方法

关键代码&#xff1a;LightmapSettings.lightmaps lightmapDatas; LightmapData中操作三张图&#xff1a;lightmapColor,lightmapDir,以及一张ShadowMap 这里只操作前两张&#xff1a; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI;public cl…

leetcode 238. 除自身以外数组的乘积

题目如下 数据范围 使用两个辅助数组分别存从前乘到后面和从后到前后面再计算就行。 &#xff08;f数组没处理好还包含了本不能乘于的数所以要向后移动一位&#xff09;。通过代码 class Solution { public:vector<int> productExceptSelf(vector<int>& n…

以太坊基金会换帅,资本市场砸盘

Vitalik力挺Aya升任EF主席&#xff0c;理想主义冬日发芽&#xff1f; 作者&#xff1a;Wenser&#xff1b;编辑&#xff1a;秦晓峰 出品 | Odaily星球日报&#xff08;ID&#xff1a;o-daily&#xff09; 2 月 27 日&#xff0c;Bybit 15 亿资金被盗事件的最新调查结果将以太坊…

[含文档+PPT+源码等]精品基于Python实现的微信小程序的在线医疗咨询系统

基于Python实现的微信小程序的乡村医疗咨询系统背景&#xff0c;可以从以下几个方面进行阐述&#xff1a; 一、社会背景 医疗资源分布不均&#xff1a;在我国&#xff0c;城乡医疗资源分布不均是一个长期存在的问题。乡村地区由于地理位置偏远、经济条件有限&#xff0c;往往…

【Maven】基于IDEA进行Maven工程的创建、构建

文章目录 一、基于IDEA创建Maven工程1. 概念梳理Maven工程的GAVP2. Idea构建Maven Java SE工程3. Idea构建Maven Java Web工程3.1 创建一个maven的javase工程3.2 修改pom.xml文件打包方式3.3 设置web资源路径和web.xml路径 4. Maven工程项目结构说明 二、基于IDEA进行Maven工程…

Halcon 学习之路 生成棋盘格 set_grayval 算子

gen_imag_const 创建灰度图像 gen_image_const(Image&#xff0c;Type&#xff0c;Width&#xff0c;Height) 算子gen_image_const创建指定大小的图像&#xff0c;图像的宽度和高度由Width和Height决定 Type 像素类型 byte :每像素1字节&#xff0c;无符号&#xff08;0-255&…

一个基于C# Winform开源免费的通用快速开发框架,内置完整的权限架构!

前言 今天大姚给大家分享一个基于C# Winform开源免费&#xff08;GPL-2.0开源协议&#xff09;的通用快速开发框架&#xff0c;内置完整的权限架构&#xff1a;WinformDevFramework。 项目介绍 WinformDevFramework是一个基于C# Winform开源免费&#xff08;GPL-2.0开源协议…

通俗解释机器学习中的召回率、精确率、准确率

先说个题外话&#xff0c;暴击一下乱写博客的人&#xff0c;网络上很多地方分不清准确率和精确率&#xff0c;在这里先正确区分一下精确率和准确率&#xff0c;以及他们的别称。 切入正题 很多人分不清召回率和精确率的区别&#xff0c;即使记住了公式&#xff0c;过段时间还是…

【数据结构】二叉树(门槛极低的系统性理解)

本篇文章将进行图文讲述该种数据结构&#xff01;看完一定不会让你失望&#xff0c;好的文章不需要过多的浮夸&#xff0c;质量就是深得人心的砝码&#xff01;下面我总结了最形象的趣味理解方法&#xff0c;一遍看完终身不忘&#xff01;制作不易&#xff0c;能否一键三连呢&a…

【漫话机器学习系列】114.逻辑 Sigmoid 函数

逻辑 Sigmoid 函数详解 1. 引言 逻辑回归&#xff08;Logistic Regression&#xff09;是机器学习中常用的分类算法&#xff0c;而 Sigmoid 函数 是逻辑回归的核心数学工具。Sigmoid 函数能够将任意实数映射到 (0,1) 之间&#xff0c;因此特别适用于概率估计。在这篇文章中&a…

SpringBoot项目启动报错:PathVariable annotation was empty on param 0.

报错信息 SpringBoot项目启动报错&#xff1a;Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name com.obstetric.archive.feignclient.DictServiceClient: FactoryBean threw exception on object creation; nested excepti…

Java 设计模式:软件开发的精髓与艺

目录 一、设计模式的起源二、设计模式的分类1. 创建型模式2. 结构型模式3. 行为型模式三、设计模式的实践1. 单例模式2. 工厂模式3. 策略模式四、设计模式的优势五、设计模式的局限性六、总结在软件开发的浩瀚星空中,设计模式犹如一颗颗璀璨的星辰,照亮了开发者前行的道路。它…

Unity小功能实现:鼠标点击移动物体

1、功能描述 当玩家点击鼠标时&#xff0c;场景中的物体会移动到鼠标点击的位置。这个功能可以用于控制角色移动、放置物体等场景。 2、实现步骤 创建Unity项目&#xff1a;首先&#xff0c;打开Unity并创建一个新的3D项目。 添加3D物体&#xff1a;在场景中创建一个3D物体&am…

避坑!用Docker搞定PHP开发环境搭建(Mac、Docker、Nginx、PHP-FPM、XDebug、PHPStorm、VSCode)

本次更新主要是对环境版本进行了更新&#xff0c;例如php 7.3.7升级到了7.3.8&#xff0c;另外之前的版本有同学踩了坑&#xff0c;主要是官方docker镜像php:7.3.7-fpm和php:7.3.8-fpm使用了不同版本的debian&#xff0c;后面会提到&#xff0c;请各位同学留意。 因为最近换电脑…

Spring Boot集成Jetty、Tomcat或Undertow及支持HTTP/2协议

目录 一、常用Web服务器 1、Tomcat 2、Jetty 3、Undertow 二、什么是HTTP/2协议 1、定义 2、特性 3、优点 4、与HTTP/1.1的区别 三、集成Web服务器并开启HTTP/2协议 1、生成证书 2、新建springboot项目 3、集成Web服务器 3.1 集成Tomcat 3.2 集成Jetty 3.3 集成…