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

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

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

今天开始,我会和大家一起深入学习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

相关推荐

MySQL进阶五之自动读写分离mysql-proxy

自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...

Postgres vs MySQL_vs2022连接mysql数据库

...

3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?

引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...

一文由浅入深带你完全掌握MySQL的锁机制原理与应用

本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...

验证Mysql中联合索引的最左匹配原则

后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...

MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)

目录1.索引基础...

你会看 MySQL 的执行计划(EXPLAIN)吗?

SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...

MySQL 从入门到精通(四)之索引结构

索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...

mysql总结——面试中最常问到的知识点

mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...

mysql总结——面试中最常问到的知识点(2)

首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...

MySQL基础全知全解!超详细无废话!轻松上手~

本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...

深入剖析 MySQL 中的锁机制原理_mysql 锁详解

在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...

Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析

引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...

MySQL基础篇:DQL数据查询操作_mysql 查

一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...

MySql:索引的基本使用_mysql索引的使用和原理

一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...