百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

从源码揭秘偏向锁的升级(偏向锁升级过程)

wptr33 2025-03-24 00:24 22 浏览

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

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

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

通过本篇文章,你能解答如下问题:

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

准备工作

正式开始分析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依赖:

  
    org.openjdk.jol  
    jol-core  
    0.16  

复制代码

使用示例:

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),调用了interp_masm_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 biased_lock: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. 锁的退出逻辑 偏向锁:macroAssembler_x86#biased_locking_exit 轻量级锁:interpreterRuntime#monitorexit ObjectSynchronizer#slow_exit ObjectSynchronizer#fast_exit

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

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

  • 重置displaced_header
  • 释放锁记录

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

总结

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

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

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

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

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

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

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

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

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

来自:
https://juejin.cn/post/7175334156996935738

相关推荐

redis的八种使用场景

前言:redis是我们工作开发中,经常要打交道的,下面对redis的使用场景做总结介绍也是对redis举报的功能做梳理。缓存Redis最常见的用途是作为缓存,用于加速应用程序的响应速度。...

基于Redis的3种分布式ID生成策略

在分布式系统设计中,全局唯一ID是一个基础而关键的组件。随着业务规模扩大和系统架构向微服务演进,传统的单机自增ID已无法满足需求。高并发、高可用的分布式ID生成方案成为构建可靠分布式系统的必要条件。R...

基于OpenWrt系统路由器的模式切换与网页设计

摘要:目前商用WiFi路由器已应用到多个领域,商家通过给用户提供一个稳定免费WiFi热点达到吸引客户、提升服务的目标。传统路由器自带的Luci界面提供了工厂模式的Web界面,用户可通过该界面配置路...

这篇文章教你看明白 nginx-ingress 控制器

主机nginx一般nginx做主机反向代理(网关)有以下配置...

如何用redis实现注册中心

一句话总结使用Redis实现注册中心:服务注册...

爱可可老师24小时热门分享(2020.5.10)

No1.看自己以前写的代码是种什么体验?No2.DooM-chip!国外网友SylvainLefebvre自制的无CPU、无操作码、无指令计数器...No3.我认为CS学位可以更好,如...

Apportable:拯救程序员,IOS一秒变安卓

摘要:还在为了跨平台使用cocos2d-x吗,拯救objc程序员的奇葩来了,ApportableSDK:FreeAndroidsupportforcocos2d-iPhone。App...

JAVA实现超买超卖方案汇总,那个最适合你,一篇文章彻底讲透

以下是几种Java实现超买超卖问题的核心解决方案及代码示例,针对高并发场景下的库存扣减问题:方案一:Redis原子操作+Lua脚本(推荐)//使用Redis+Lua保证原子性publicbo...

3月26日更新 快速施法自动施法可独立设置

2016年3月26日DOTA2有一个79.6MB的更新主要是针对自动施法和快速施法的调整本来内容不多不少朋友都有自动施法和快速施法的困扰英文更新日志一些视觉BUG修复就不翻译了主要翻译自动施...

Redis 是如何提供服务的

在刚刚接触Redis的时候,最想要知道的是一个’setnameJhon’命令到达Redis服务器的时候,它是如何返回’OK’的?里面命令处理的流程如何,具体细节怎么样?你一定有问过自己...

lua _G、_VERSION使用

到这里我们已经把lua基础库中的函数介绍完了,除了函数外基础库中还有两个常量,一个是_G,另一个是_VERSION。_G是基础库本身,指向自己,这个变量很有意思,可以无限引用自己,最后得到的还是自己,...

China&#39;s top diplomat to chair third China-Pacific Island countries foreign ministers&#39; meeting

BEIJING,May21(Xinhua)--ChineseForeignMinisterWangYi,alsoamemberofthePoliticalBureau...

移动工作交流工具Lua推出Insights数据分析产品

Lua是一个适用于各种职业人士的移动交流平台,它在今天推出了一项叫做Insights的全新功能。Insights是一个数据平台,客户可以在上面实时看到员工之间的交流情况,并分析这些情况对公司发展的影响...

Redis 7新武器:用Redis Stack实现向量搜索的极限压测

当传统关系型数据库还在为向量相似度搜索的性能挣扎时,Redis7的RedisStack...

Nginx/OpenResty详解,Nginx Lua编程,重定向与内部子请求

重定向与内部子请求Nginx的rewrite指令不仅可以在Nginx内部的server、location之间进行跳转,还可以进行外部链接的重定向。通过ngx_lua模块的Lua函数除了能实现Nginx...