JVM偏向锁的前世今生以及“退位“真相

作为一名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会执行以下流程:

  1. 检查对象头的Mark Word:看看这个锁是否处于可偏向状态
  2. 判断偏向线程ID:如果处于偏向状态,检查记录的线程ID是否是当前线程
    • 如果是当前线程,直接获取锁,不需要CAS操作
    • 如果不是当前线程,需要撤销偏向锁
  3. CAS操作:如果对象处于可偏向状态但还没有偏向任何线程,那么会使用CAS操作将当前线程ID写入对象头

你可能会好奇,这些操作是在哪里实现的呢?实际上,它们是在JVM的C++代码中实现的,主要在synchronizer.cpp中。

我记得第一次阅读这部分源码时,感觉像是发现了一个隐藏的宝藏——“哦,原来锁是这样实现的!”

3.2 偏向锁的撤销过程

在这里插入图片描述

偏向锁的撤销是一个复杂的流程,因为涉及到线程安全点(Safepoint)的概念:

  1. 检查偏向线程状态

    • 如果线程已经死亡,可以直接撤销偏向锁
    • 如果线程还活着,需要等待它到达安全点
  2. 等待安全点:JVM会在合适的时机让所有线程都进入安全点,这个过程叫做"Stop The World"(STW)

  3. 分析线程栈

    • 检查偏向的线程是否还需要持有锁(是否还在同步块内)
    • 如果需要继续持有锁,则升级为轻量级锁
    • 如果不需要,则恢复为无锁状态

偏向锁撤销是导致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 撤销的常见原因

偏向锁在以下几种情况下会被撤销:

  1. 不同线程竞争锁:当有另一个线程尝试获取已经被偏向的锁

  2. 调用对象的hashCode方法:因为偏向锁的实现会占用对象头中存储hashCode的位置

  3. 批量重偏向:当同一个类的大量对象的偏向锁被撤销时,JVM会考虑是否要批量重偏向

  4. 批量撤销:如果对某个类的对象撤销偏向次数过多,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不仅会对单个对象的偏向锁进行处理,还会采取一些批量操作来提高效率:

  1. 批量重偏向(Bulk Rebias)

    • 当某个类的对象频繁地发生偏向锁撤销时(默认阈值是20),JVM会认为这个类的对象的锁竞争比较激烈
    • 此时,JVM会对这个类的所有对象进行重偏向,将它们偏向于新的线程
  2. 批量撤销(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 偏向锁的劣势

然而,偏向锁并非没有缺点:

  1. 撤销成本高:偏向锁撤销过程比较复杂,需要等待全局安全点,这会导致STW(Stop-The-World)暂停

  2. 不适合高竞争场景:如果锁竞争频繁,偏向锁会频繁升级,反而会降低性能

  3. 对GC不友好:批量重偏向和批量撤销可能会影响GC性能

在我的项目实践中,有一些高竞争的场景,我们甚至会在启动参数中关闭偏向锁来获得更好的性能:

-XX:-UseBiasedLocking

5.3 何时需要关注偏向锁撤销

什么情况下你需要重点关注偏向锁撤销呢?

  1. 系统出现性能抖动:如果你的系统响应时间出现不规律的波动,可能是偏向锁撤销导致的STW

  2. 高并发场景:在高并发系统中,频繁的锁竞争会导致大量的偏向锁撤销

  3. 频繁创建和销毁线程的场景:比如线程池中有大量短生命周期的任务

我曾经调试过一个系统,通过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决定废弃偏向锁呢?主要有以下原因:

  1. 现代硬件的变化:多核CPU已经成为主流,单线程场景的优化价值降低

  2. 锁实现的复杂性:偏向锁为JVM增加了大量复杂代码,维护成本高

  3. 应用模式的变化:现代Java应用更多采用无锁编程、细粒度锁、并发集合等

  4. 性能提升有限:在大多数现代应用中,偏向锁带来的性能提升有限,但撤销开销依然存在

// JDK 15及以上使用偏向锁需要
// -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

6.3 我还应该关注偏向锁吗?

虽然偏向锁被废弃了,但在以下情况下,你仍然需要关注它:

  1. 如果你使用的是JDK 15以下版本:偏向锁仍然是默认开启的

  2. 对于有严格性能要求的系统:了解偏向锁的原理可以帮助你根据实际情况选择开启或关闭

  3. 理解JVM锁机制:偏向锁是理解JVM锁升级机制的重要环节

我经常说:"理解历史是为了更好地把握未来。"即使偏向锁将被淘汰,它的设计思想依然值得学习。

七、面试中的偏向锁(八股文时间到)

7.1 常见面试题

作为一个经常面试Java工程师的人,我总结了一些关于偏向锁的高频面试题:

  1. Q: 偏向锁是什么?为什么要引入偏向锁?
    A: 偏向锁是JVM为了减少同一线程获取锁的开销而引入的一种优化。因为在大多数情况下,锁不存在多线程竞争,而是由同一个线程多次获取。

  2. Q: 偏向锁的获取和撤销流程是怎样的?
    A: 获取:检查锁是否可偏向 -> 检查线程ID是否为当前线程 -> 如果是则直接获取,如果不是则需要撤销。
    撤销:等待全局安全点 -> 暂停线程 -> 检查锁状态 -> 升级为轻量级锁或恢复为无锁状态。

  3. Q: 什么情况下会导致偏向锁撤销?
    A: 不同线程竞争锁、调用对象hashCode方法、批量重偏向达到阈值、批量撤销达到阈值等。

  4. Q: JDK 15为什么要废弃偏向锁?
    A: 主要是由于现代硬件和应用特点变化,偏向锁带来的收益已经不足以抵消其复杂性和维护成本。

很多候选人在回答这些问题时只能说出概念,却不能解释清楚原理,这往往会影响面试结果。

7.2 如何回答加分

想要在面试中脱颖而出,可以尝试从以下角度回答偏向锁相关问题:

  1. 联系实际场景:说明哪些场景适合使用偏向锁,哪些场景不适合

  2. 结合JVM参数:展示你对JVM调优的了解,比如如何开启、关闭偏向锁,以及如何调整相关阈值

  3. 谈论性能影响:解释偏向锁对系统性能的影响,包括积极影响和消极影响

  4. 提及最新动态:比如JDK 15废弃偏向锁的消息,表明你关注技术发展动态

一个好的回答不仅仅是背诵概念,更是要展示你对技术的深入理解和实际应用经验。

八、总结与反思

偏向锁是Java并发编程中的一个精巧设计,它利用"大多数情况下,锁不存在竞争"这一特性来优化性能。虽然它在JDK 15中被废弃,但学习它的设计思想依然有价值。

回顾整个偏向锁的设计,我们可以得到几点启示:

  1. 针对常见场景优化:软件设计应该针对最常见的场景进行优化,这是偏向锁设计的核心思想

  2. 权衡取舍:任何优化都有代价,偏向锁以撤销的复杂性为代价换取了单线程场景的性能提升

  3. 与时俱进:随着硬件和应用模式的变化,曾经的优化可能变得不再适用,需要及时调整

  4. 理解本质:深入理解技术的本质原理,才能在实际应用中做出正确的选择

在职业生涯中,偏向锁的设计思路启我,有时候"偏心"也是一种优化策略,只要清楚代价并能接受潜在的影响。

写在最后

偏向锁就像一个忠诚的老朋友,在Java 6到Java 14的漫长岁月里,默默地为程序提速。虽然即将告别历史舞台,但它的设计思想值得每一位程序员学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

慢德

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值