作为一名Java架构师,不得不说,JVM的锁机制非常复杂,这篇文章详细介绍JVM偏向锁。文章有点长,但保证干货满满。
一、什么是偏向锁?(这不是我第一次问自己这个问题)
1.1 基本概念
偏向锁是JVM为了提高锁性能而引入的一种锁优化机制。它的核心思想非常简单:大多数情况下,锁总是由同一个线程获得,为何不让这个线程"偏心"一点呢?
public synchronized void doSomething() {
// 代码块
System.out.println("我是被锁保护的代码");
}
在上面这段代码中,如果99%的时间都是同一个线程在调用这个方法,那么每次都走一遍完整的锁获取和释放流程,是不是有点浪费?
偏向锁的出现就是为了解决这个问题:如果一个线程获得了锁,那么锁就会偏向该线程,下次该线程进入同步块时,不需要再做任何同步操作。
我第一次了解到这个概念时,心想:"这也太偏心了吧!"但不得不承认,这个设计确实很巧妙。
1.2 偏向锁的标识
在Java对象头的Mark Word中,有一个偏向锁标识位和偏向线程ID。
当一个对象被创建后,如果开启了偏向锁(默认开启),对象的Mark Word会被设置为可偏向状态。当第一个线程来获取锁时,会将自己的线程ID写入对象头的Mark Word中,此时锁就进入了"偏向"状态。
二、偏向锁的应用场景(真实世界中的"偏心")
2.1 最佳应用场景
偏向锁最适合的场景是:同一个线程多次获取同一个锁。
比如:
public class BiasedLockDemo {
private static final Object lock = new Object();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
synchronized (lock) {
// 每次都是main线程获取锁
System.out.println("第" + (i + 1) + "次获取锁");
}
}
}
}
在这个例子中,main线程连续10次获取同一个锁,这就是偏向锁的理想场景。第一次获取锁时,偏向锁会将线程ID记录到对象头的Mark Word中,之后的9次获取锁,由于是同一个线程,所以不需要做同步操作,直接获取锁。
我经常把偏向锁比喻成"VIP通道"——只要你是特定的那个人,就可以直接通过,不需要任何检查。
2.2 实际应用中的考量
在实际开发中,我们需要考虑:
- 应用程序中的锁竞争程度
- 锁获取的频率
- 获取锁的线程是否总是相同
如果你的程序中,锁总是被同一个线程获取,那么偏向锁将极大地提升性能;但如果锁经常被不同的线程获取,那么偏向锁反而可能会降低性能。
三、偏向锁的实现原理(揭开神秘面纱)
3.1 偏向锁的获取过程
当线程尝试获取偏向锁时,JVM会执行以下流程:
- 检查对象头的Mark Word:看看这个锁是否处于可偏向状态
- 判断偏向线程ID:如果处于偏向状态,检查记录的线程ID是否是当前线程
- 如果是当前线程,直接获取锁,不需要CAS操作
- 如果不是当前线程,需要撤销偏向锁
- CAS操作:如果对象处于可偏向状态但还没有偏向任何线程,那么会使用CAS操作将当前线程ID写入对象头
你可能会好奇,这些操作是在哪里实现的呢?实际上,它们是在JVM的C++代码中实现的,主要在synchronizer.cpp
中。
我记得第一次阅读这部分源码时,感觉像是发现了一个隐藏的宝藏——“哦,原来锁是这样实现的!”
3.2 偏向锁的撤销过程
偏向锁的撤销是一个复杂的流程,因为涉及到线程安全点(Safepoint)的概念:
-
检查偏向线程状态:
- 如果线程已经死亡,可以直接撤销偏向锁
- 如果线程还活着,需要等待它到达安全点
-
等待安全点:JVM会在合适的时机让所有线程都进入安全点,这个过程叫做"Stop The World"(STW)
-
分析线程栈:
- 检查偏向的线程是否还需要持有锁(是否还在同步块内)
- 如果需要继续持有锁,则升级为轻量级锁
- 如果不需要,则恢复为无锁状态
偏向锁撤销是导致JVM性能抖动的一个常见原因,因为它需要STW。
3.3 偏向锁的底层实现
偏向锁的核心实现在HotSpot VM的biasedLocking.cpp
文件中:
// 这是简化版的代码,实际实现更复杂
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(...) {
// 1. 判断是否允许偏向锁
if (!UseBiasedLocking) {
return BiasedLocking::NOT_BIASED;
}
// 2. 检查对象头信息
markOop mark = obj->mark();
if (!mark->has_bias_pattern()) {
return BiasedLocking::NOT_BIASED;
}
// 3. 获取偏向的线程信息
JavaThread* biased_thread = mark->biased_locker();
if (biased_thread == NULL) {
// ...
}
// 4. 判断是否是当前线程
if (biased_thread == current) {
// ...
}
// 5. 需要撤销偏向锁...
// 等待安全点、遍历线程栈、升级锁等操作
}
看到这种底层实现,我总是想到那句程序员的名言:“底层代码比文档更能说明问题。”
四、偏向锁的撤销条件(何时说再见)
4.1 撤销的常见原因
偏向锁在以下几种情况下会被撤销:
-
不同线程竞争锁:当有另一个线程尝试获取已经被偏向的锁
-
调用对象的hashCode方法:因为偏向锁的实现会占用对象头中存储hashCode的位置
-
批量重偏向:当同一个类的大量对象的偏向锁被撤销时,JVM会考虑是否要批量重偏向
-
批量撤销:如果对某个类的对象撤销偏向次数过多,JVM会对该类的所有对象都撤销偏向锁,甚至禁止新创建的该类对象的偏向
举个实际例子:
四、偏向锁的撤销条件
4.1 撤销的常见原因
public class BiasedLockRevokeDemo {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 让main线程获取锁,触发偏向
synchronized (lock) {
System.out.println("Main thread acquires lock first time");
}
// 创建新线程来竞争锁,会导致偏向锁撤销
Thread t = new Thread(() -> {
synchronized (lock) {
System.out.println("Another thread acquires the lock, biased lock revoked");
}
});
t.start();
t.join();
// 再次获取锁,此时已经不是偏向锁状态
synchronized (lock) {
System.out.println("Main thread acquires lock again, but no longer biased");
}
}
}
当第二个线程尝试获取锁时,JVM会发现这个锁已经偏向于main线程,此时会触发偏向锁撤销,锁会升级为轻量级锁。
这就像一个"专属通道"突然被大家发现了,不得不升级成普通通道,让所有人都能公平使用。
4.2 批量重偏向与批量撤销
JVM不仅会对单个对象的偏向锁进行处理,还会采取一些批量操作来提高效率:
-
批量重偏向(Bulk Rebias):
- 当某个类的对象频繁地发生偏向锁撤销时(默认阈值是20),JVM会认为这个类的对象的锁竞争比较激烈
- 此时,JVM会对这个类的所有对象进行重偏向,将它们偏向于新的线程
-
批量撤销(Bulk Revoke):
- 如果批量重偏向仍然频繁发生(默认阈值是40),JVM会认为这个类的对象根本不适合使用偏向锁
- 此时,JVM会撤销该类所有对象的偏向锁,并且新创建的该类对象也不会启用偏向锁
你可以通过以下JVM参数调整这些阈值:
-XX:BiasedLockingBulkRebiasThreshold=20 # 批量重偏向阈值
-XX:BiasedLockingBulkRevokeThreshold=40 # 批量撤销阈值
我曾经在一个高并发系统中,通过调整这些参数来减少偏向锁撤销带来的性能波动,效果还是挺明显的。
五、偏向锁的性能影响(成也萧何,败也萧何)
5.1 偏向锁的优势
偏向锁显著地提升了单线程重复获取锁的性能:
import java.util.concurrent.TimeUnit;
public class LockPerformanceTest {
private static final int ITERATIONS = 10_000_000;
private static final Object lock = new Object();
public static void main(String[] args) throws Exception {
// 预热,确保JIT编译器优化过的代码
for (int i = 0; i < 100000; i++) {
synchronized (lock) {
// 空操作
}
}
// 测试偏向锁性能(单线程情况)
long start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
synchronized (lock) {
// 空操作
}
}
long end = System.nanoTime();
System.out.printf("偏向锁模式(单线程): %,d ns,平均每次: %,d ns%n",
(end - start), (end - start) / ITERATIONS);
// 添加参数 -XX:-UseBiasedLocking 可以关闭偏向锁
// 添加参数 -XX:BiasedLockingStartupDelay=0 可以立即启用偏向锁
}
}
在我的开发机上,上面这段代码在开启偏向锁(默认)和关闭偏向锁(通过-XX:-UseBiasedLocking
)的情况下,性能差异大约是5倍左右。这就是为什么偏向锁在JDK 6中被引入并默认开启的原因。
5.2 偏向锁的劣势
然而,偏向锁并非没有缺点:
-
撤销成本高:偏向锁撤销过程比较复杂,需要等待全局安全点,这会导致STW(Stop-The-World)暂停
-
不适合高竞争场景:如果锁竞争频繁,偏向锁会频繁升级,反而会降低性能
-
对GC不友好:批量重偏向和批量撤销可能会影响GC性能
在我的项目实践中,有一些高竞争的场景,我们甚至会在启动参数中关闭偏向锁来获得更好的性能:
-XX:-UseBiasedLocking
5.3 何时需要关注偏向锁撤销
什么情况下你需要重点关注偏向锁撤销呢?
-
系统出现性能抖动:如果你的系统响应时间出现不规律的波动,可能是偏向锁撤销导致的STW
-
高并发场景:在高并发系统中,频繁的锁竞争会导致大量的偏向锁撤销
-
频繁创建和销毁线程的场景:比如线程池中有大量短生命周期的任务
我曾经调试过一个系统,通过JVM参数-XX:+PrintBiasedLockingStatistics
和-XX:+TraceDeflation
来观察偏向锁的使用情况,从而识别出导致性能问题的地方。
六、JDK 15中偏向锁的命运(时代的眼泪)
6.1 偏向锁的废弃
在JDK 15中,偏向锁被标记为废弃(Deprecated):
JEP 374: Disable and Deprecate Biased Locking
这意味着从JDK 15开始:
- 偏向锁默认关闭
- 需要通过
-XX:+UseBiasedLocking
显式开启 - 计划在未来的JDK版本中完全移除
当我第一次看到这个消息时,心里有点难过:“啊,又一个老朋友要离开了。”
6.2 为什么要废弃偏向锁?
为什么Oracle决定废弃偏向锁呢?主要有以下原因:
-
现代硬件的变化:多核CPU已经成为主流,单线程场景的优化价值降低
-
锁实现的复杂性:偏向锁为JVM增加了大量复杂代码,维护成本高
-
应用模式的变化:现代Java应用更多采用无锁编程、细粒度锁、并发集合等
-
性能提升有限:在大多数现代应用中,偏向锁带来的性能提升有限,但撤销开销依然存在
// JDK 15及以上使用偏向锁需要
// -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
6.3 我还应该关注偏向锁吗?
虽然偏向锁被废弃了,但在以下情况下,你仍然需要关注它:
-
如果你使用的是JDK 15以下版本:偏向锁仍然是默认开启的
-
对于有严格性能要求的系统:了解偏向锁的原理可以帮助你根据实际情况选择开启或关闭
-
理解JVM锁机制:偏向锁是理解JVM锁升级机制的重要环节
我经常说:"理解历史是为了更好地把握未来。"即使偏向锁将被淘汰,它的设计思想依然值得学习。
七、面试中的偏向锁(八股文时间到)
7.1 常见面试题
作为一个经常面试Java工程师的人,我总结了一些关于偏向锁的高频面试题:
-
Q: 偏向锁是什么?为什么要引入偏向锁?
A: 偏向锁是JVM为了减少同一线程获取锁的开销而引入的一种优化。因为在大多数情况下,锁不存在多线程竞争,而是由同一个线程多次获取。 -
Q: 偏向锁的获取和撤销流程是怎样的?
A: 获取:检查锁是否可偏向 -> 检查线程ID是否为当前线程 -> 如果是则直接获取,如果不是则需要撤销。
撤销:等待全局安全点 -> 暂停线程 -> 检查锁状态 -> 升级为轻量级锁或恢复为无锁状态。 -
Q: 什么情况下会导致偏向锁撤销?
A: 不同线程竞争锁、调用对象hashCode方法、批量重偏向达到阈值、批量撤销达到阈值等。 -
Q: JDK 15为什么要废弃偏向锁?
A: 主要是由于现代硬件和应用特点变化,偏向锁带来的收益已经不足以抵消其复杂性和维护成本。
很多候选人在回答这些问题时只能说出概念,却不能解释清楚原理,这往往会影响面试结果。
7.2 如何回答加分
想要在面试中脱颖而出,可以尝试从以下角度回答偏向锁相关问题:
-
联系实际场景:说明哪些场景适合使用偏向锁,哪些场景不适合
-
结合JVM参数:展示你对JVM调优的了解,比如如何开启、关闭偏向锁,以及如何调整相关阈值
-
谈论性能影响:解释偏向锁对系统性能的影响,包括积极影响和消极影响
-
提及最新动态:比如JDK 15废弃偏向锁的消息,表明你关注技术发展动态
一个好的回答不仅仅是背诵概念,更是要展示你对技术的深入理解和实际应用经验。
八、总结与反思
偏向锁是Java并发编程中的一个精巧设计,它利用"大多数情况下,锁不存在竞争"这一特性来优化性能。虽然它在JDK 15中被废弃,但学习它的设计思想依然有价值。
回顾整个偏向锁的设计,我们可以得到几点启示:
-
针对常见场景优化:软件设计应该针对最常见的场景进行优化,这是偏向锁设计的核心思想
-
权衡取舍:任何优化都有代价,偏向锁以撤销的复杂性为代价换取了单线程场景的性能提升
-
与时俱进:随着硬件和应用模式的变化,曾经的优化可能变得不再适用,需要及时调整
-
理解本质:深入理解技术的本质原理,才能在实际应用中做出正确的选择
在职业生涯中,偏向锁的设计思路启我,有时候"偏心"也是一种优化策略,只要清楚代价并能接受潜在的影响。
写在最后
偏向锁就像一个忠诚的老朋友,在Java 6到Java 14的漫长岁月里,默默地为程序提速。虽然即将告别历史舞台,但它的设计思想值得每一位程序员学习。