本文的 原始地址 (带本文的学习视频),传送门
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,很多小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试机会,遇到很多很重要的面试题:
1.请解释 JVM 偏向锁、轻量级锁、自旋锁、重量级锁什么?
2.请介绍一下什么是sychronized的自旋锁、偏向锁、轻量级锁、重量级锁?
3.请介绍一下 jvm 内置锁 的膨胀过程?
4.请介绍一下 jvm 内置锁 的膨胀过程中锁内存怎么变化的?
5.请介绍一下 jvm 内置锁 的 从轻量级锁升级重量级锁内存怎么变化的?
6.请介绍一下 jvm 锁的 膨胀过程?锁内存怎么变化的?
最近有小伙伴在面试阿里,又遇到了 jvm 内置锁 膨胀相关的面试题。小伙伴 支支吾吾的说了几句,没说清楚,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,上面的面试题以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
1 面试背景
阿里面试中考察JVM锁结构 的膨胀过程及锁内存变化,主要是因为JVM锁机制是Java并发编程中的核心知识点,对于理解和优化多线程程序的性能至关重要。
阿里作为互联网头部企业,对候选人的JVM底层原理和高并发场景优化能力要求极高。
该问题属于典型的高阶面试题,旨在考察:
- 对synchronized锁底层机制的掌握深度,涉及偏向锁、轻量级锁、重量级锁等核心概念;
- 对锁状态转换触发条件和性能影响的理解,要求结合实际场景分析锁优化策略;
- 内存结构变化的底层逻辑,如对象头(Mark Word)在不同锁状态下的存储方式
2 考察重点
2.1. 锁的状态及膨胀过程
考察候选人是否清楚 JVM 中锁的不同状态,如无锁、偏向锁、轻量级锁和重量级锁,以及这些状态之间是如何转换的,即锁的膨胀过程。例如,在什么情况下会从偏向锁升级为轻量级锁,又在什么情况下会从轻量级锁升级为重量级锁。
2.2. 锁内存的变化
了解候选人是否知道在锁状态转换过程中,对象头的内存布局是如何变化的。对象头中存储了锁的状态信息,不同的锁状态对应着不同的对象头内容,如偏向锁会在对象头中记录偏向的线程 ID,轻量级锁会使用 CAS 操作在对象头中存储锁记录的指针等。
2.3. 并发编程知识和实践经验
通过这个问题,可以考察候选人对并发编程的理解和实践经验。例如,候选人是否知道如何根据不同的场景选择合适的锁,如何避免锁的过度使用和锁竞争,以及如何优化并发程序的性能等。
3. 回答的 核心要点
3.1 锁膨胀过程
- 偏向锁:
首次线程访问时,通过CAS将线程ID写入Mark Word,后续同一线程无需同步操作
- 轻量级锁:
当多个线程交替执行(非竞争),JVM在栈帧创建Lock Record,通过CAS将Mark Word指向该记录
- 重量级锁
当自旋失败(竞争激烈),Mark Word指向操作系统维护的Monitor对象,线程进入阻塞队列。
3.2 锁内存变化
- 偏向锁撤销 内存变化:
Mark Word中线程ID被清除,升级为轻量级锁时生成Lock Record;
- 轻量级锁膨胀 内存变化:
Mark Word从指向Lock Record变为指向Monitor,同时释放原Lock Record空间;
- 重量级锁 内存变化 内存开销:
Monitor对象包含EntryList、WaitSet等数据结构,占用额外堆内存。
4、回答策略建议
- 结构化表述:
按“膨胀阶段→内存变化→性能影响”分点回答,避免逻辑混乱;
- 结合场景举例:
如 单例模式初始化场景 (使用 偏向锁)、短任务并发(轻量级锁)、强竞争任务场景(重量级锁);
- 提升一下,介绍一下优化手段:
如减少锁粒度、锁粗化、关闭偏向锁(-XX:-UseBiasedLocking)等。
为啥内置锁存在多种状态?
在JDK1.6版本之前,所有的Java内置锁都是重量级锁。重量级锁会造成CPU在用户态和核心态之间频繁切换,所以代价高、效率低。
JDK1.6版本为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”实现。
所以,在JDK1.6版本里内置锁一共有四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这些状态随着竞争情况逐渐升级。
内置锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种能升级却不能降级的策略,其目的是为了提高获得锁和释放锁的效率。
为什么会存在锁升级现象?
在 synchronized
最初的实现方式是 “阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间。
在java5及其以前,只有synchronized 这个是重量级锁,是操作系统级别的重量级操作。
重量级锁两大问题:
-
如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长,
-
假如锁的竞争比较激烈,性能下降。
因为重量级锁 存在用户态和内核态之间的转换。
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程,就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,
用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
这也是在JDK6以前 synchronized
效率低下的原因,JDK6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
锁升级现象存在的核心原因在保证线程安全的前提下,通过动态调整锁的粒度(偏向锁→轻量级锁→重量级锁)来平衡性能开销与并发效率:
无竞争时用低开销锁, 减少同步损耗(如偏向锁仅需一次CAS),
竞争加剧时逐步升级锁机制以匹配实际并发强度(如重量级锁通过阻塞避免CPU空转)
一图总览:锁的内存变化及膨胀流程图
二、锁的四种状态
这种方式就是 synchronized
实现同步最初的方式,这也是当初开发者诟病的地方,
所以目前锁状态一种有四种,从级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁,锁状态只能升级,不能降级
锁状态的思路以及特点
这种方式就是 synchronized
实现同步最初的方式,这也是当初开发者诟病的地方,
所以目前锁状态一种有四种,从级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁,锁状态只能升级,不能降级
锁状态的思路以及特点
锁状态 | 存储内容 | 标志位 |
---|---|---|
无锁 | 对象的hashCode、对象分代年龄、是否是偏向锁(0) | 01 |
偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向互斥量的指针 | 11 |
如图所示:
在32位的虚拟机中:
在64位的虚拟机中:
锁对比 锁机制 性能开销分析 形象分析
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到索竞争的线程,使用自旋会消耗CPU | 追求响应速度,同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
**1. 微竞争场景(单线程/无竞争) → 偏向锁(手枪)**
- 类比 偏向锁类似手枪,**
手枪轻便、单发,适合单兵快速反应。
仅需一次CAS操作记录线程ID**,后续无需重复加锁,直接进入同步代码=。
- 性能开销
无额外资源消耗(如自旋、线程阻塞),仅维护对象头中的线程ID。
- 适用场景
单线程重复访问锁(如缓存、局部变量锁)。
**2. 弱竞争场景(少量线程交替竞争) → 轻量级锁(机枪)**
-
类比 机枪射速快、火力持续,适合小规模对抗。轻量级锁通过CAS自旋 短暂等待锁释放,避免线程阻塞。
-
性能开销
自旋消耗CPU资源(类似机枪持续消耗弹药),但避免内核态切换(弱竞争下效率高于重量级锁)。
- 适用场景
短时间低并发(如简单计数器、短任务队列)。
**3. 强竞争场景(高并发激烈竞争) → 重量级锁(大炮)**
- 类比
大炮威力大但笨重,适合全面战争。重量级锁依赖**操作系统互斥量(Mutex)**,强制线程阻塞并排队。
- 性能开销
线程切换、内核态切换(类似大炮需要多人操作和资源调度),资源消耗最高。
- 适用场景
长时间高并发(如数据库连接池、全局资源竞争)。
锁升级与性能开销对比
锁类型 | 枪械类比 | 竞争强度 | 性能开销 | 核心机制 |
---|---|---|---|---|
偏向锁 | 手枪 | 微竞争 | 极低(仅首次CAS | 线程ID记 |
轻量级锁 | 机枪 | 弱竞争 | 中(自旋消耗CPU | CAS自旋 |
重量级锁 | 大炮 | 强竞争 | 高(线程阻塞/切换 | 操作系统互斥量 |
关键结论
-
锁升级不可逆 一旦从低开销锁升级到高开销锁(如轻量级→重量级),无法降级。
-
开销与场景匹配
手枪(偏向锁)在误用强竞争时失效 → 需升级锁类型。
大炮(重量级锁)在弱竞争时浪费资源 → 应避免过早膨胀。
- 设计原则 根据实际竞争强度选择锁机制,优先使用轻量级锁,仅在必要时升级。
2.4.1 偏向锁/轻量级锁/重量级锁
总体而言,Java对象(Object实例)结构包括三部分:对象头、对象体、对齐字节。具体如图2-4所示。
1. Java对象(Object实例)的三个部分
(1)对象头
对象头包括三个字段,第一个字段叫做_mark Word(标记字),用于存储自身运行时的数据例如GC标志位、哈希码、锁状态等信息。
第二个字段叫做_klass
Pointer(类对象指针),用于存放此对象的元数据(InstanceKlass)的地址。通过_klass
指针,虚拟机通过可以确定这个对象是哪个类的实例.
第二个字段叫做Array Length(数组长度)。如果对象是一个Java数组,那么此字段必须有,用于记录数组长度的数据;如果对象不是一个Java数组,那么此字段不存在,所以这是一个可选字段。
(2)对象体
对象体包含了对象的实例变量(成员变量)。用于成员属性值,包括父类的成员属性值。这部分内存按4字节对齐。
(3)对齐字节
对齐字节也叫做填充对齐,其作用是用来保证Java对象在所占内存字节数为8的倍数(8N bytes)。HotSpot VM的内存管理要求对象起始地址必须是8字节的整数倍。对象头本身是8的倍数,当对象的实例变量数据不是8的倍数,便需要填充数据来保证8字节的对齐。
2. 对象结构中的核心字段作用
接下来,对Object实例结构中几个重要的字段的作用做一下简要说明:
(1)_mark(标记字)字段主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode。
(2)_klass(类对象指针)字段是一个指向方法区中类元数据信息的指针,意味着该对象可随时知道自己是哪个Class(实际为InstanceKlass)的实例。
(3)Array Length(数组长度)字段也占用32位(在32位JVM中)的字节,这是可选的,只有当本对象是一个数组对象时才会有这个部分。
(4)对象体用于保存对象属性值,是对象的主体部分,占用的内存空间大小取决于对象的属性数量和类型。
(5)对齐字节并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。当对象实例数据部分没有对齐(8字节的整数倍)时,就需要通过对齐填充来补全。
3. 对象结构中的字段长度
Mark Word(表_mark成员)、_klass
Pointer(表_klass成员)、Array Length等字段的长度,都与JVM的位数有关。
Mark Word的长度为JVM的一个Word(字)大小,也就是说32位JVM的Mark Word为32位,64位JVM为64位。Klass Pointer(类对象指针)字段的长度也为JVM的一个Word(字)大小,即32位的JVM为32位,64位的JVM为64位。
所以,
-
在32位JVM虚拟机中,Mark Word和Klass Pointer这两部分都是32位的;
-
在64位JVM虚拟机中,Mark Word和Klass Pointer这两部分都是64位的。
对于对象指针而言,如果JVM中对象数量过多,使用64位的指针将浪费大量内存,通过简单统计,64位的JVM将会比32位的JVM多耗费50%的内存。
为了节约内存可以使用选项+UseCompressedOops开启指针压缩。
选项UseCompressedOops中的Oop部分为Ordinary object pointer普通对象指针的缩写。
如果开启UseCompressedOops选项,以下类型的指针将从64位压缩至32位:
2.4.2 Mark Word的结构信息
Java内置锁的涉及到很多重要信息,这些都存放在对象结构中,并且是存放于对象头的 Mark Word字段中。Mark Word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。
Mark Word的位长度不会受到Oop对象指针压缩选项的影响。
Java内置锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁。
其实在 JDK 1.6之前,Java内置锁还是一个重量级锁,是一个效率比较低下的锁,在JDK 1.6之后,JVM为了提高锁的获取与释放效率,对synchronized的实现进行了优化,引入了偏向锁、轻量级锁的实现,从此以后Java内置锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别)。
不同锁状态下的Mark word字段结构
Mark word字段的结构,与Java内置锁的状态强相关。为了让Mark word字段存储更多的信息,JVM将Mark word的最低两个位设置为Java内置锁状态位,不同锁状态下的32位Mark Word结构,如表2-1所示。
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。
如果没有 面试机会,可以找尼恩来帮忙,打造一个绝世好简历,实现 职业逆袭:中厂大龄34岁,被裁8月收一大厂offer, 年薪65W,转架构后逆天改命!
1. 无锁状态
Java对象刚创建时,还没有任何线程来竞争,说明该对象处于无锁状态(无线程竞争它)这偏向锁标识位是0、锁状态01。
无锁状态下对象的Mark Word如图2-7所示。
图2-7 无锁状态对象的Mark Word
2. 偏向锁状态
偏向锁是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。如果内置锁处于偏向状态,当有一个线程来竞争锁时,先用偏向锁,表示内置锁偏爱这个线程,这个线程要执行该锁关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下,效率非常高。
偏向锁状态的Mark Word会记录内置锁自己偏爱的线程ID,内置锁会将该线程当做自己的熟人。偏向锁状态下对象的Mark Word具体如图2-8所示。
图2-8 偏向锁状态内置锁的Mark Word
3. 轻量级锁状态
当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录。轻量级锁状态下对象的Mark Word具体如图2-9所示。
图2-9 轻量级锁状态内置锁的Mark Word
当锁处于偏向锁的时候,而又被另一个线程所企图抢占时,偏向锁就会升级为轻量级锁。企图抢占的线程会通过自旋的形式尝试获取锁,不会阻塞抢锁线程,以便提高性能。
自旋原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗 CPU的,如果一直获取不到锁,那线程也不能一直占用 CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。
JVM 对于自旋周期的选择,JDK1.6之后引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不是固定的,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,自旋不会一直持续下去,这时争用线程会停止自旋进入阻塞状态,该锁膨胀为重量级锁。
4. 重量级锁状态
重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器(Monitor)对象,该监视器对象用集合的形式,来登记和管理排队的线程。重量级锁状态下对象的Mark Word具体如图2-10所示。
图2-10 重量级锁状态内置锁的Mark Word
三:如何获得偏向锁
下面的一段代码,如何获得偏向锁?
// 使用 synchronized 关键字创建一个同步代码块
// 确保同一时间只有一个线程可以进入该代码块
// lock 是一个对象,作为锁的标识,线程需要获取这个对象的锁才能进入同步代码块
synchronized (lock) {
// 调用 lock 对象的 increase() 方法
// 通常这个方法会对 lock 对象的内部状态进行修改,
// 由于在同步代码块中,保证了线程安全
lock.increase();
// 检查变量 i 的值是否等于 MAX_TURN 的一半
// MAX_TURN 应该是一个预先定义的常量,表示某个循环的最大次数
if (i == MAX_TURN / 2) {
// 调用 Print 类的 tcfo 方法,打印一条信息
// 信息内容为 "占有锁, lock 的状态: ",用于提示当前线程已经获取了锁
Print.tcfo("占有锁, lock 的状态: ");
// 调用 lock 对象的 printObjectStruct() 方法
// 这个方法通常用于打印 lock 对象的内部结构或状态信息
lock.printObjectStruct();
}
}
偏向锁的核心原理是:如果不存在 竞争的一个线程获得了锁,那么锁就从无锁状态,进入偏向状态,此时,Mark Word 的结构变为偏向锁结构,
-
锁对象的锁标志位(lock)被改为01,
-
偏向标志位(biased_lock)被改为1,
-
然后线程的thread ID记录在锁对象的Mark Word中(使用CAS操作完成)。
以后该线程获取锁的时,判断一下线程ID和标志位,就可以直接进入同步块,连CAS操作都不需要,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
偏向锁比较极致,干脆就把同步取消掉,不需要进行CAS了。
偏向锁的发现,主要得益于人们发现某个线程可以频繁的获取到锁。
偏向锁其实就是为了单个线程设计的。
如果某个锁资源一直是被某个线程获取,而且没有其它线程来获取锁,就可以在Mark Word
中记录下这个线程id,该线程就没有必要花时间来进行CAS操作了,可以直接进入到同步代码块。
直到发现有其它线程来抢占锁资源了,就会根据当前状态判断是否把偏向锁膨胀成为轻量级锁。
如果需要使用偏向锁,可以使用参数:-XX:+UseBiased
参数来添加。
在JDK1.6之后是默认开启的,但是启动时间有延迟(4秒),
在JDK1.6之后,需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动
开启偏向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:关闭之后程序默认会直接进入------->轻量级锁状态
-XX:-UseBiasedLocking
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。
如果没有 面试机会,可以找尼恩来帮忙,打造一个绝世好简历,实现 职业逆袭:中厂大龄34岁,被裁8月收一大厂offer, 年薪65W,转架构后逆天改命!
四:如何膨胀到 轻量级锁
多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞。
有线程来参与锁的竞争,但是获取锁的冲突时间极短。
轻量级锁本质就是自旋锁CAS
主要目的: 在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞。
升级时机:当关闭偏向锁功能,或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。
而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。
此时线程B操作中有两种情况:
如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A→B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程“被“释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;
如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。
如果没有 面试机会,可以找尼恩来帮忙,打造一个绝世好简历,实现 职业逆袭:中厂大龄34岁,被裁8月收一大厂offer, 年薪65W,转架构后逆天改命!
轻量级锁也是在 JDK1.6 加入的,当一个线程获取偏向锁的时候,有另外的线程加入锁的竞争时,这个时候就会从偏向锁升级为轻量级锁。
在轻量级锁的状态时,虚拟机首先会在当前线程的栈帧当中建立一个锁记录(Lock Record),用于存储对象 MarkWord 的拷贝,官方称这个为 Displaced Mark Word。
然后虚拟机会使用 CAS 操作尝试将对象的 MarkWord 指向栈中的 Lock Record,如果操作成功说明这个线程获取到了锁,能够进入同步代码块执行,否则说明这个锁对象已经被其他线程占用了,线程就需要使用 CAS 不断的进行获取锁的操作,当然你可能会有疑问,难道就让线程一直死循环了吗?
这对 CPU 的花费那不是太大了吗,确实是这样的因此在 CAS 满足一定条件的时候轻量级锁就会升级为重量级锁,具体过程在重量级锁章节中分析。
当线程需要从同步代码块出来的时候,线程同样的需要使用 CAS 将 Displaced Mark Word 替换回对象的 MarkWord,如果替换成功,那么同步过程就完成了,如果替换失败就说明有其他线程尝试获取该锁,而且锁已经升级为重量级锁,此前竞争锁的线程已经被挂起,因此线程在释放锁的同时还需要将挂起的线程唤醒。
Java6之前
默认启用,默认情况下自旋的次数是10次,或者自旋线程数超过cpu核数一半。
Java6之后
变为自适应自旋锁。意味着自旋的次数不是固定不变的,而是根据:拥有锁线程的状态来决定,或者同一个锁上一次自旋的时间。
线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。
反之,如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。
在有两个以上的线程竞争同一个轻量级锁的情况下,轻量级锁不再有效(轻量级锁升级的一个条件),这个时候锁为膨胀成重量级锁,锁的标志状态变成 10,MarkWord 当中存储的就是指向重量级锁的指针,后面等待锁的线程就会被挂起。
因为这个时候 MarkWord 当中存储的已经是指向重量级锁的指针,因此在轻量级锁的情况下进入到同步代码块在出同步代码块的时候使用 CAS 将 Displaced Mark Word 替换回对象的 MarkWord 的时候就会替换失败,在前文已经提到,在失败的情况下,线程在释放锁的同时还需要将被挂起的线程唤醒。
五:如何膨胀到 重量级锁
适用于:有大量的线程参与锁的竞争,冲突性很高。
重量级锁原理
Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。
重量级锁就是一种开销最大的锁机制,
在这种情况下需要操作系统将 没有抢到锁的线程挂起,
JVM(Linux 操作系统下)底层是使用 pthread_mutex_lock 、 pthread_mutex_unlock 、 pthread_cond_wait 、 pthread_cond_signal 和 pthread_cond_broadcast 这几个库函数实现的,而这些函数依赖于 futex 系统调用,因此在使用重量级锁的时候因为进行了系统调用,进程需要从用户态转为内核态将线程挂起,然后从内核态转为用户态,当解锁的时候又需要从用户态转为内核态将线程唤醒,这一来二去的花费就比较大了(和 CAS 自旋锁相比)。
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。
如果没有 面试机会,可以找尼恩来帮忙,打造一个绝世好简历,实现 职业逆袭:中厂大龄34岁,被裁8月收一大厂offer, 年薪65W,转架构后逆天改命!
锁的内存结构变化 大总结
锁状态 | bits | 1bit是否是偏向锁 | 2bit锁标志位 |
---|---|---|---|
无锁状态 | 对象的hashCode | 0 | 01 |
偏向锁 | 线程ID | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 0 | 00 |
重量级锁 | 指向互斥量的指针 | 0 | 10 |
锁的膨胀流程 以及JVM锁膨胀过程中,线程栈、对象头、Monitor堆结构的具体变化
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。
如果没有 面试机会,可以找尼恩来帮忙,打造一个绝世好简历,实现 职业逆袭:中厂大龄34岁,被裁8月收一大厂offer, 年薪65W,转架构后逆天改命!
JVM锁膨胀过程中,线程栈、对象头、Monitor堆结构的具体变化如下:
1. 无锁 → 偏向锁
- 对象头结构变化
原无锁状态(标志位 01
)的Mark Word中写入偏向线程ID、偏向时间戳等数据,标志位变为偏向锁状态 101
。此时Mark Word不再存储哈希码(偏向锁启用时禁用哈希码生成)。
- 线程栈结构变化
未创建锁记录(Lock Record),仅通过CAS操作将偏向线程ID写入对象头。
- Monitor堆结构
未创建Monitor对象,无变化。
2. 偏向锁 → 轻量级锁
- 对象头结构变化
当其他线程尝试获取锁时,撤销偏向锁:原Mark Word(含偏向线程ID)被复制到线程栈的锁记录中,对象头替换为指向锁记录的指针,标志位变为轻量级锁 00
。
- 线程栈结构变化
每个竞争线程在栈帧中创建独立的锁记录(Lock Record),存储原对象头Mark Word的拷贝,并通过CAS尝试将对象头指向自身的锁记录。
- Monitor堆结构 仍无Monitor对象,锁竞争通过CAS自旋解决。
3. 轻量级锁 → 重量级锁
- 对象头结构变化
当自旋失败或竞争激烈时,锁膨胀为重量级锁:Mark Word替换为指向堆中Monitor对象的指针,标志位变为 10
。
- 线程栈结构变化
轻量级锁的锁记录被释放,竞争失败的线程进入阻塞状态,并被记录到Monitor的EntryList中。
- Monitor堆结构
在堆中创建Monitor对象(由ObjectMonitor实现),包含以下核心字段:
_owner
:指向持有锁的线程。
_EntryList
:存储等待锁的线程队列。
_WaitSet
:存储调用wait()
方法的线程队列。
关键差异总结
锁状态 | 对象头标志位 | 线程栈结构 | Monitor堆结构 |
---|---|---|---|
无锁 | 01 | 无锁记录 | 无 |
偏向锁 | 101 | 无锁记录 | 无 |
轻量级锁 | 00 | 锁记录存储原Mark Word | 无 |
重量级锁 | 10 | 锁记录释放 | Monitor对象创建 |
附加说明
- 锁膨胀过程中,哈希码和GC分代年龄的存储位置会随锁状态变化而调整:偏向锁禁用哈希码存储,轻量级锁将哈希码暂存于锁记录,重量级锁则可能重新生成哈希码。
- 重量级锁的线程阻塞依赖操作系统互斥量(Mutex),而Monitor对象通过
_cxq
(竞争队列)和_EntryList
实现线程调度13。
知识扩展1:三种锁使用的场景,背后的原理分析
为什么 单例模式初始化场景 使用 偏向锁 ?
为什么 短任务并发 轻量级锁 ?
为什么 强竞争任务池 重量级锁?
单例模式初始化场景使用偏向锁
单例模式初始化场景分析
单例模式确保一个类只有一个实例,并提供一个全局访问点。
在大多数情况下,单例对象的初始化是由一个线程完成的,后续的访问也是由同一个线程频繁进行。 所以呢,竞争激烈的程度是很弱的。
例如,在一个应用程序中,数据库连接池的单例实例,通常在启动时由一个线程初始化,之后在程序运行期间也主要由这个线程或少数几个线程频繁访问。
偏向锁的特性
偏向锁的设计初衷是为了在只有一个线程访问同步块的场景下,尽量减少锁的开销。
当一个线程第一次访问同步块并获取锁时,JVM 会在对象头中记录该线程的 ID,以后该线程再次进入这个同步块时,无需进行任何同步操作,直接可以获取锁,避免了 CAS(Compare - And - Swap)操作的开销。
单例模式初始化场景使用偏向锁的 原因
单例模式初始化场景 ,竞争激烈的程度是很弱的。
在单例模式初始化场景中,由于通常是单个线程完成初始化并后续频繁访问,使用偏向锁可以让该线程在后续的访问中几乎没有锁的开销,从而提高性能。
如果使用轻量级锁或重量级锁,每次访问都需要进行 CAS 操作或线程阻塞唤醒等操作,会带来不必要的性能损耗。
短任务并发场景使用轻量级锁
短任务并发场景分析
短任务并发是指多个线程执行的任务执行时间较短,并且这些任务之间可能会有对共享资源的竞争。
例如,在一个多线程的计算任务中,每个线程负责计算一个小的数据块,这些线程可能会同时访问一个共享的计数器来记录计算结果的数量。
轻量级锁的特性
轻量级锁是为了在多线程交替执行同步块的场景下,避免重量级锁的线程阻塞和唤醒带来的性能开销。当线程尝试获取轻量级锁时,会使用 CAS 自旋操作将对象头中的 Mark Word 替换为指向锁记录的指针。
如果成功,线程获得锁;如果失败,线程会进行自旋等待,而不是立即阻塞。自旋等待是指线程会在原地循环检查锁是否被释放,这个过程不会进行线程上下文切换,开销相对较小。
短任务并发场景 ,竞争激烈的程度是中等的。
但是,轻量级锁 cas 自旋 ,可能会带来 大量资源消耗, 甚至由于大量的 总线通讯,带来总线风暴。
使用轻量级锁原因
在短任务并发场景中,由于任务执行时间短,如果线程竞争锁失败后立即阻塞,那么线程的阻塞和唤醒操作所带来的开销可能会比任务本身的执行时间还要长。
而轻量级锁的自旋等待机制可以让线程在短时间内等待锁的释放,避免了线程阻塞和唤醒的开销,从而提高了性能。
强竞争任务 场景使用重量级锁
强竞争任务 特点
强竞争任务 是指多个线程频繁竞争同一个共享资源的场景。
例如,在一个高并发的消息队列中,多个消费者线程同时竞争从队列中取出消息进行处理。
重量级锁特性
重量级锁依赖于操作系统的互斥量(Mutex)来实现,当一个线程持有重量级锁时,其他线程会被阻塞,进入内核态进行线程调度。
虽然重量级锁的加锁和解锁操作开销较大,但是它可以保证在高竞争场景下的线程安全,避免了大量的自旋等待带来的 CPU 资源浪费。
综合原因
在强竞争任务池场景中,由于线程竞争非常激烈,如果使用轻量级锁,线程可能会在长时间内自旋等待,消耗大量的 CPU 资源。而重量级锁可以让竞争失败的线程进入阻塞状态,释放 CPU 资源,当持有锁的线程释放锁后,操作系统会唤醒等待的线程,这样可以更有效地利用 CPU 资源,保证系统的稳定性和性能。
高手炫技,介绍一下优化手段:
尼恩提示,讲完 如减少锁粒度、锁粗化、关闭偏向锁(-XX:-UseBiasedLocking)等优化手段 , 可以得到 120分了。
如减少锁粒度、锁粗化、关闭偏向锁(-XX:-UseBiasedLocking)等。
1 减少锁粒度
减少锁粒度就是把原本一个大的锁保护的范围变小,让锁只锁住那些真正需要同步的小部分代码或数据,而不是将一大块代码都用锁保护起来。
在编程里也是一样,比如有一个数据结构是多个列表的集合,原来对整个集合加锁,现在可以对每个列表单独加锁,这样不同线程就可以同时操作不同的列表,提高并发性能。
2 锁粗化
锁粗化和减少锁粒度相反,它是把多个连续的加锁、解锁操作, 合并成一个更大范围的加锁、解锁操作。简单来说,就是把锁的范围适当扩大。
在编程中,如果代码里有连续的对同一个对象加锁和解锁操作,就可以把这些操作合并成一次加锁和一次解锁,减少锁的开销。
3 关闭偏向锁
偏向锁是 JVM 为了在只有一个线程访问同步块时提高性能而设计的一种锁机制。
关闭偏向锁就是不使用这种锁机制,让锁直接从无锁状态进入其他状态(如轻量级锁或重量级锁)。
在编程里,如果程序中同步块的竞争比较激烈,很多线程会交替访问同步块,那么偏向锁的撤销和重偏向操作会带来额外的开销,这时候关闭偏向锁,让锁直接进入更适合竞争场景的状态,能提高程序的性能。
说在最后:有问题找老架构取经
JVM锁的膨胀、锁的内存结构变化相关的面试题,是非常常见的面试题。
也是核心面试题。也是非常难面试题。
以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典》V174,在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来帮扶、领路。尼恩已经指导了大量的就业困难的小伙伴上岸.
前段时间,帮助一个40岁+就业困难小伙伴拿到了一个年薪100W的offer,小伙伴实现了 逆天改命 。