揭秘偏向锁的升级

news2024/11/26 18:25:59

今天开始,我会和大家一起深入学习synchronized的原理,原理部分会涉及到两篇:

  • 偏向锁升级到轻量级锁的过程
  • 轻量级锁升级到重量级锁的过程

今天我们先来学习偏向锁升级到轻量级锁的过程。因为涉及到大量HotSpot源码,会有单独的一篇注释版源码的文章。

通过本篇文章,你们解答synchronized都问啥?中统计到的如下问题:

  • 详细描述下synchronized的实现原理
  • 为什么说synchronized是可重入锁?
  • 详细描述下synchronized的锁升级(膨胀)过程
  • 偏向锁是什么?synchronized是怎样实现偏向锁的?
  • Java 8之后,synchronized做了哪些优化?

准备工作

正式开始分析synchronized源码前,我们先做一些准备:

  • HotSpot源码准备:Open JDK 11;
  • 字节码工具,推荐jclasslib插件
  • 用于跟踪对象状态的jol-core包。

Tips

  • 可以使用javap命令和IDEA自带的字节码工具;
  • jclasslib的优势在于可以直接跳转到相关命令的官方站点。

示例代码

准备一个简单的示例代码:

public class SynchronizedPrinciple {

	private int count = 0;

	private void add() {
		synchronized (this) {
			count++;
		}
	}
}

通过工具,我们可以得到如下字节码:

aload_0
dup
astore_1
monitorenter // 1
aload_0
dup
getfield #2 <com/wyz/keyword/synckeyword/SynchronizedPrinciple.count : I>
iconst_1
iadd
putfield #2 <com/wyz/keyword/synckeyword/SynchronizedPrinciple.count : I>
aload_1
monitorexit // 2
goto 24 (+8)
astore_2
aload_1
monitorexit // 3
aload_2
athrow
return

synchronized修饰代码块,编译成了两条指令:

  • monitorenter:进入对象的监视器;
  • monitorexit:退出对象的监视器。

我们注意到,monitorexit出现了两次。注释2的部分是程序执行正常,注释3的部分是程序执行异常。Java团队连程序异常的情况都替你考虑到了,他真的,我哭死。

Tips

  • 使用synchronized修饰代码块作为示例的原因是,修饰方法时仅在access_flag设置ACC_SYNCHRONIZED标志,并不直观;
  • Java并不是只能通过monitorexit退出监视器,Java曾在Unsafe类中提供过进出监视器的方法
Unsafe.getUnsafe.monitorEnter(obj);
Unsafe.getUnsafe.monitorExit(obj);

Java 8可以使用,Java 11已经移除,具体移除的版本我就不太清楚了。

jol使用示例

可以通过jol-core来跟踪对象状态。Maven依赖:

<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.16</version>
</dependency>

使用示例:

public static void main(String[] args) {
	Object obj = new Object();
	System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

从monitorenter处开始

在HotSpot中,monitorenter指令对应这两大类解析方式:

  • 字节码解释器:bytecodeInterpreter
  • 模板解释器:templateTable_x86#monitorenter

由于bytecodeInterpreter基本退出了历史舞台,我们以模板解释器X86实现templateTable_x86为例。

Tips

  • 按照惯例,源码只展示关键内容;
  • 推荐杨易老师的《深入解析Java虚拟机HotSpot》。

monitorenter的执行方法是templateTable_x86#monitorenter该方法中,我们只需要关注4438行执行的__lock_object(rmon),调用了interpmasm_x86#lock_object方法:

void InterpreterMacroAssembler::lock_object(Register lock_reg) {
	if (UseHeavyMonitors) {// 1
		// 重量级锁逻辑
		call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),  lock_reg);
	} else {
		Label done;
		Label slow_case;
		if (UseBiasedLocking) {// 2
			// 偏向锁逻辑
			biased_locking_enter(lock_reg, obj_reg, swap_reg, tmp_reg, false, done, &slow_case);
		}

		// 3
		bind(slow_case);
		call_VM(noreg,   CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),  lock_reg);
		bind(done);
		......
			}

注释1和注释2的部分,是两个JVM参数:

// 启用重量级锁
-XX:+UseHeavyMonitors
// 启用偏向锁
-XX:+UseBiasedLocking

注释1和注释3,调用InterpreterRuntime::monitorenter方法,注释1是直接使用重量级锁的配置,那么可以猜到,注释3是获取偏向锁失败锁升级为重量级锁的逻辑。

对象头(markOop)

正式开始前,先来了解对象头(markOop)。实际上,markOop的注释已经揭露了它的“秘密“:

The markOop describes the header of an object.

…..

Bit-format of an object header (most significant first, big endian layout below):

64 bits:

unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)

JavaThread:54 epoch:2 unused:1 age:4 biasedlock:1 lock:2 (biased object)

……

[JavaThread_ | epoch | age | 1 | 01] lock is biased toward given thread

[0 | epoch | age | 1 | 01] lock is anonymously biase

注释详细的描述了64位大端模式下Java对象头的结构:

Tips

  • 也描述了32位markOop的结构,我没粘出来~~
  • markOop锁标志枚举

对象头中的大部分结构都很容易理解,但epoch是什么?

注释中将epoch描述为“used in support of biased locking”。OpenJDK wiki中Synchronization是这样描述epoch的:

An epoch value in the class acts as a timestamp that indicates the validity of the bias.

epoch类似于时间戳,表示偏向锁的有效性。它的在批量重偏向阶段(biasedLocking#bulk_revoke_or_rebias_at_safepoint)更新:

static BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o, bool bulk_rebias, bool attempt_rebias_of_object, JavaThread* requesting_thread) {
	{
		if (bulk_rebias) {
			if (klass->prototype_header()->has_bias_pattern()) {
				klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
			}
		}
	}
}

JVM通过epoch来判断是否适合偏向锁,超过阈值后JVM会升级偏向锁。JVM提供了参数来调节这个阈值。

// 批量重偏向阈值
-XX:BiasedLockingBulkRebiasThreshold=20
// 批量撤销阈值
-XX:BiasedLockingBulkRevokeThreshold=40

Tips:更新的是klass的epoch。

偏向锁(biasedLocking)

系统开启了偏向锁,会进入macroAssembler_x86#biased_locking_enter方法。该方法首先是获取对象的markOop:

Address mark_addr         (obj_reg, oopDesc::mark_offset_in_bytes());
Address saved_mark_addr(lock_reg, 0);

我将接下来的流程分为5个分支,按照执行顺序和大家一起分析偏向锁的实现逻辑。

Tips

  • 了解偏向锁流程即可,因此以图示为主,源码分析放在偏向锁源码分析中;
  • 偏向锁源码分析以注释为主,详细标注了每个分支;
  • 这部分实际上包含了撤销重偏向两个跳转标签,分支图示中有说明;
  • 源码使用位掩码技术,为了便于区分,二进制数字用0B开头,并补齐4位。

分支1:是否可偏向?

偏向锁的前置条件,逻辑非常简单,判断当前对象markOop的锁标志,如果已经升级,执行升级流程;否则继续向下执行。

Tips:虚线部分逻辑位于其它类中。

分支2:是否重入偏向?

目前JVM已知markOop的锁标志位为0B0101,处于可偏向状态,但不清楚是已经偏向还是尚未偏向。HotSopt中使用anonymously形容可偏向但尚未偏向某个线程的状态,称这种状态为匿名偏向。此时对象头如下:

此时要做的事情就比较简单了,判断是否为当前线程重入偏向锁。如果是重入,直接退出即可;否则继续向下执行。

Tips:今天刷到一个帖子,Javaer和C++er争论可重入锁和递归锁,有兴趣的可以看一文看懂并发编程中的锁我简单解释了可重入锁和递归锁的关系。

分支3:是否依旧可偏向?

注释描述了不是重入偏向锁的情况:

At this point we know that the header has the bias pattern and that we are not the bias owner in the current epoch. We need to figure out more details about the state of the header in order to know what operations can be legally performed on the object's header.

此时可能存在两种情况:

  • 不存在竞争,重新偏向某个线程;
  • 存在竞争,尝试撤销。

偏向锁撤销的部分稍微复杂,使用对象klass的markOop替换对象的markOop,关键技术是CAS

分支4:epoch是否过期?

目前偏向锁的状态是可偏向,且偏向其他线程。此时的逻辑只需要片段epoch是否有效即可。

重新偏向的可以用一句话描述,构建markOop进行CAS替换。

分支5:重新偏向

目前偏向锁的状态是,可偏向,偏向其它线程,epoch未过期。此时要做的是在markOop中设置当前线程,也就是偏向锁重新偏向的过程,和分支4的部分非常相似。

撤销和重偏向

获取偏向锁失败后,执行InterpreterRuntime::monitorenter方法,位于interpreterRuntime中:

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
if (UseBiasedLocking) {
	// 完整的锁升级路径
	// 偏向锁->轻量级锁->重量级锁
	ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
	// 跳过偏向锁的锁升级路径
	// 轻量级锁->重量级锁
	ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
IRT_END

ObjectSynchronizer::fast_enter位于synchronizer.cpp#fast_enter:

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
	if (UseBiasedLocking) {
		if (!SafepointSynchronize::is_at_safepoint()) {
			// 撤销和重偏向
			BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj,  attempt_rebias, THREAD);
			if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
				return;
			}
		} else {
			BiasedLocking::revoke_at_safepoint(obj);
		}
	}
	// 跳过偏向锁
	slow_enter(obj, lock, THREAD);
}

BiasedLocking::revoke_and_rebias的精简注释版放在了偏向锁源码分析的第2部分。

轻量级锁(basicLock)

如果获取偏向锁失败,此时会执行ObjectSynchronizer::slow_enter,该方法位于synchronizer#slow_enter:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
	markOop mark = obj->mark();
	// 无锁状态 ,获取偏向锁失败后有撤销逻辑,此时变为无锁状态
	if (mark->is_neutral()) {
		// 将对象的markOop复制到displaced_header(Displaced Mark Word)上
		lock->set_displaced_header(mark);
		// CAS将对象markOop中替换为指向锁记录的指针
		if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
			// 替换成功,则获取轻量级锁
			TEVENT(slow_enter: release stacklock);
			return;
		}
	} else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
		//  重入情况
		lock->set_displaced_header(NULL);
		return;
	}
	// 重置displaced_header(Displaced Mark Word)
	lock->set_displaced_header(markOopDesc::unused_mark());
	// 锁膨胀
	ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}

直接引用《Java并发编程的艺术》中关于轻量级锁加锁的过程:

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁的逻辑非常简单,使用到的关键技术也是CAS。此时markOop的结构如下:

在monitorexit处结束

处于偏向锁或者轻量级锁时,monitorexit的逻辑非常简单。有了monitorenter的经验,我们很容易分析到monitorexit的调用逻辑:

  1. templateTable_x86#monitorexit
  2. interp_masm_x86#un_lock
  3. 锁的退出逻辑
  4. 偏向锁:macroAssembler_x86#biased_locking_exit
  5. 轻量级锁:interpreterRuntime#monitorexit
  6. ObjectSynchronizer#slow_exit
  7. ObjectSynchronizer#fast_exit

代码就留给大家自行探索了,在这里给出我的理解。

通常,我会简单的认为偏向锁退出时,什么都不需要做(即偏向锁不会主动释放);而对于轻量级锁来说,至少需要经历两个步骤:

  • 重置displaced_header
  • 释放锁记录

因此,从退出逻辑上来说,轻量级锁的性能是稍逊于偏向锁的。

总结

我们对这一阶段的内容做个简单的总结,偏向锁和轻量级锁的逻辑并不复杂,尤其是轻量级锁。

偏向锁和轻量级锁的关键技术都是CAS,当CAS竞争失败,说明有其它线程尝试抢夺,从而导致锁升级。

偏向锁在对象markOop中记录第一次持有它的线程,当该线程不断持有偏向锁时,只需要简单的比对即可,适合绝大部分场景是单线程执行,但偶尔可能会存在线程竞争的场景。

但问题是,如果线程交替持有执行,偏向锁的撤销和重偏向逻辑复杂,性能差。因此引入了轻量级锁,用来保证交替进行这种“轻微”竞争情况的安全。

另外,关于偏向锁的争议比较多,主要在两点:

  • 偏向锁的撤销对性能影响较大;
  • 大量并发时,偏向锁非常鸡肋。

实际上,Java 15中已经放弃了偏向锁(JEP 374: Deprecate and Disable Biased Locking),但由于大部分应用还跑在Java 8上,我们还是要了解偏向锁的逻辑。

最后再辟个谣(或者是被打脸?),轻量级锁中并没有任何自旋的逻辑

Tips:好像漏掉了批量撤销和批量重偏向~~


如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠王有志,我们下次再见!

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

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

相关文章

从钉钉到金蝶云星空通过接口配置打通数据

从钉钉到金蝶云星空通过接口配置打通数据 对接系统钉钉 钉钉&#xff08;DingTalk&#xff09;是阿里巴巴集团打造的企业级智能移动办公平台&#xff0c;是数字经济时代的企业组织协同办公和应用开发平台。钉钉将IM即时沟通、钉钉文档、钉闪会、钉盘、Teambition、OA审批、智能…

python | 将pdf文件转换为图片,这一招就够了

一、背景 部分情况下&#xff0c;需要将 PDF 页面转换为图片&#xff0c;例如 PNG 或 JPEG 格式。 python 的开源库 pdfplumber&#xff0c;提供了将 pdf 文件转换为图片的方法。 如果之前还没有安装和使用过pdfplumber库&#xff0c;pdfplumber的安装及基础使用&#xff0c;可…

【React学习】—SetState的使用(九)

【React学习】—SetState的使用&#xff08;九&#xff09; state的简写方式 state属性总结

PCD格式点云文件结构及在线查看工具

本文档描述了 PCD&#xff08;点云数据&#xff09;文件格式及其在点云库&#xff08;PCL&#xff09;中的使用方式。可以使用NSDT 3DConvert 在线预览查看PCD格式的点云数据文件。 推荐&#xff1a;用 NSDT编辑器 快速搭建可编程3D场景 1、为何定义新的点云数据文件格式&#…

【问题总结+备忘录】上传一个shp文件能够读取其中的空间矢量字段,代码+采坑总结

需求描述 要求上传一个shp文件能够读取其中的空间矢量字段。 简单分析 SHP上传格式应该有两种&#xff08;zip格式和.shp的格式文件内部可能存在多个空间矢量&#xff0c;结果以列表形式返回文件不大&#xff0c;使用MultipartFile上传上传即可结合geo-tools读取空间字段&am…

【C++进阶(二)】STL大法--vector的深度剖析以及模拟实现

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:C从入门到精通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习C   &#x1f51d;&#x1f51d; vector 1. 前言2. 熟悉vector的接口函数2.1 vec…

无涯教程-Python机器学习 - Reinforcement Learning函数

这些方法不同于以前研究的方法,也很少使用。在这种学习算法中,我们需要在一段时间内训练一个代理,以便它可以与特定环境交互。代理将遵循一系列与环境进行交互的策略,然后在观察环境之后,它将针对环境的当前状态采取措施。以下是强化学习方法的主要步骤。 第1步-首先,我们需要准…

金鸣表格文字识别软件,让你的图片瞬间变成excel

来百度APP畅享高清图片 首先&#xff0c;让我们打开金鸣表格文字识别软件&#xff01; 然后点击那个“添加文件”的按钮&#xff0c;在弹出的窗口中选择你想要识别的图片&#xff0c;再点击“打开”&#xff0c;就可以把图片添加到待识别的列表中了。 接下来&#xff0c;你只需…

领星ERP和金蝶云星空接口打通对接实战

领星ERP和金蝶云星空接口打通对接实战 对接系统&#xff1a;领星ERP 深圳市领星网络科技有限公司创立于2017年&#xff0c;致力于通过SaaSERP产品为跨境电商行业创造价值&#xff0c;让跨境生意更简单。公司总部位于深圳&#xff0c;在广州、杭州、厦门等设有服务中心。领星现已…

[计算机入门] 账户管理

3.4 账户管理 用户账户是计算机操作系统中用于标识和管理用户身份的概念。 每个用户都拥有一个唯一的用户账户&#xff0c;该账户包含用户的登录名、密码和其他与用户身份相关的信息。 用户账户通常用于验证用户身份&#xff0c;并授权对系统资源的访问权限。在多用户操作系统…

SSL证书申请

DV SSL证书申请需要多久&#xff1f; DV SSL证书无需验证所有者资质资料&#xff0c;审核流程相对简单&#xff0c;因此可快速签发。但部分域名信息可能会触发不同等级的安全审查机制&#xff0c;必要时需要人工介入进行审查签发&#xff0c;因此&#xff0c;SSL证书签发时间可…

【OpenCV • c++】图像对比度调整 | 图像亮度调整

&#x1f680; 个人简介&#xff1a;CSDN「博客新星」TOP 10 &#xff0c; C/C 领域新星创作者&#x1f49f; 作 者&#xff1a;锡兰_CC ❣️&#x1f4dd; 专 栏&#xff1a;【OpenCV • c】计算机视觉&#x1f308; 若有帮助&#xff0c;还请关注➕点赞➕收藏&#xff…

GIS全国技能大赛试题及解题过程

1、提供数据列表如下&#xff1a; 2、试题要求如下&#xff1a; 3、解题步骤如下&#xff1a; &#xff08;1.1&#xff09;导入土地利用数据&#xff0c;如下&#xff1a; &#xff08;1.2&#xff09;导入土地采集表格&#xff0c;然后右键选择显示XY数据&#xff0c;如下&am…

python二维索引转一维索引 多维索引转一维索引

将二维索引转为1维索引 原博客地址&#xff1a;https://blog.csdn.net/qq_42424677/article/details/123011642 将n维索引映射成1维索引 def ravel_index(x, dims):i 0for dim, j in zip(dims, x):i * dimi jreturn i

Cesium 叠加天地图-中国近海海洋等深面图层服务

Cesium 叠加天地图-中国近海海洋等深面图层服务 核心代码完整代码&#xff1a;在线示例 偶然发现天地图有一个近海海洋图层&#xff0c;觉得不错&#xff0c;于是尝试叠加一下&#xff0c;花费了一些时间&#xff0c;叠加成功&#xff0c;这里分享一下。 本文包括核心代码、完…

Google Zxing依赖在linux服务器上生成二维码图片中带有中文显示不了的问题。

一&#xff0c;问题描述 在二维码中显示的图片中含有中文&#xff0c;不显示而是显示乱码。这是因为linux系统中为安装中文。 二&#xff0c;解决方法 1、查看所有字体&#xff1a;fc-list // 如果提示 fc-list: command not found&#xff0c;则需要安装# yum install font…

智慧排水监测系统:提高城市排水管理效率

随着城市化进程的不断推进&#xff0c;城市排水系统在城市正常运行和居民生活品质方面扮演着至关重要的角色。然而&#xff0c;随着城市化进程的加速和气候变化的加剧&#xff0c;城市排水系统面临前所未有的挑战&#xff0c;城市内涝、雨污分流不到位、河道黑臭杂乱、水体污染…

c语言练习题36:删除指定的数

删除指定的数 题目&#xff1a; 先输⼊5个整数存放在数组中&#xff0c;再输⼊⼀个整数n&#xff0c;删除数组中所有等于n的数字&#xff0c;数组中剩余的数 组保证数组的最前⾯&#xff0c;打印剩余的数字。 思路&#xff1a; 使⽤两个指针 i 和 j 。 1. i 从前往后扫描整…

网络服务第三次作业

正向解析 1.关闭服务端与客户端防火墙和SElinux 2.修改服务端与客户端的IP为静态IP地址 3.安装DNS软件 服务端IP客户端IP 网址 192.168.50.128192.168.50.131www.openlab.com. ##服务端&#xff1a; [rootquantou ~]# setenforce 0 ##关闭SELinux [rootquantou ~]# syste…

苹果通讯录怎么导入新手机?换了新手机的朋友请看这里

通讯录联系人、照片、视频和文件都是苹果手机中非常重要的数据。当人们更换手机时&#xff0c;照片、视频等文件可以通过“微信发送”或者“隔空投送”一键传输到新手机。 那么&#xff0c;苹果通讯录怎么导入新手机呢&#xff1f;小编给大家总结了2个简单转移通讯录的方法&am…