深入理解Java锁原理(一):偏向锁的设计原理与性能优化

深入理解Java锁原理(一):偏向锁的设计原理与性能优化

如果大家对偏向锁有一定了解,可以直接往后看:深入理解Java锁原理(二):轻量级锁的设计原理到实战优化

一、引言

在Java多线程编程中,锁是实现线程安全的重要工具。然而,传统的锁机制(如重量级锁)存在较大的性能开销,尤其是在无竞争的场景下。为了优化这种情况,Java 6引入了偏向锁(Biased Locking),它通过预测锁的使用模式,将无竞争场景下的锁获取和释放成本降为零。本文将深入探讨偏向锁的设计原理、释放机制以及性能优化,帮助开发者更好地理解和使用这一高效的锁机制。

二、偏向锁的核心设计动机

偏向锁的设计基于"锁使用的二八定律":在实际应用中,大部分锁在其生命周期内仅被同一个线程获取,不存在多线程竞争。传统的无锁状态虽然简单,但每次获取锁仍需执行CAS(Compare-and-Swap)操作,而CAS操作虽然轻量,但相比简单的内存比较仍有显著开销。

偏向锁通过消除无竞争场景下的同步原语,进一步提升性能。当同一线程多次获取锁时,偏向锁只需比较Thread ID(一次内存读取操作),而无锁状态仍需执行CAS操作(原子性的读-改-写操作)。

三、偏向锁的实现原理

3.1 对象头与Mark Word

在HotSpot虚拟机中,每个对象的对象头(Object Header)包含两部分信息:Mark Word和Klass Pointer。其中,Mark Word存储了对象的哈希码、分代年龄、锁状态等信息。在64位虚拟机中,Mark Word的结构如下:

当对象处于偏向锁状态时,Mark Word会存储持有锁的线程ID。

3.2 结构解析与关键说明

1. 无锁状态(Normal)

布局:25位未使用 + 31位对象哈希码 + 1位未使用 + 4位分代年龄核心特征:

哈希码存储在Mark Word中(调用hashCode()时生成)分代年龄用于GC分代收集锁状态位为01(最低两位)

2. 偏向锁状态(Biased)

布局:54位线程ID + 2位epoch(偏向时间戳) + 1位未使用 + 4位分代年龄核心特征:

直接存储持有锁的线程IDepoch用于标记偏向锁的有效性(避免跨代重用)锁状态位仍为01,但通过高位区分偏向模式

3. 轻量级锁状态(Lightweight Locked)

布局:62位指向栈中锁记录(Lock Record)的指针核心特征:

无锁状态的Mark Word被复制到线程栈帧中通过指针指向锁记录实现加锁锁状态位为00

4. 重量级锁状态(Heavyweight Locked)

布局:62位指向Monitor对象的指针核心特征:

指向ObjectMonitor结构体涉及内核态与用户态切换锁状态位为10

5. GC标记状态(Marked for GC)

布局:1位标记 + 01状态位核心特征:

用于GC时的对象标记锁状态位为11(仅最低两位有效)

3.3 偏向锁的获取流程

偏向锁的获取流程如下:

从时序图可以看出,当锁已偏向当前线程时,获取锁的操作只需比较Thread ID,无需任何同步操作,成本极低。

四、偏向锁的释放机制

偏向锁的释放机制是其高性能的核心优势之一。与传统锁不同,偏向锁的释放无需任何操作,锁继续保持偏向该线程的状态。

4.1 释放零成本的底层实现

偏向锁的释放无需操作的根本原因在于其"状态持久化"设计:Mark Word中直接存储持有锁的线程ID,锁的"偏向"状态会一直保持,直到发生竞争。当同一个线程再次获取锁时,只需比较Mark Word中的Thread ID是否与当前线程一致,这个比较操作仅需一次内存读取,成本极低。

4.2 释放流程示例

public class BiasedLockExample {

private final Object lock = new Object();

public void method() {

synchronized (lock) { // 首次获取锁:CAS设置偏向锁

// 业务逻辑

} // 释放锁:无需任何操作,锁仍偏向当前线程

synchronized (lock) { // 再次获取锁:仅验证Thread ID

// 快速获取锁,无需同步操作

}

}

}

4.3 与其他锁释放机制的对比

锁类型释放操作成本偏向锁无操作,锁保持偏向状态零成本轻量级锁CAS将Mark Word恢复为原状态一次原子操作重量级锁修改Monitor状态,唤醒EntryList中的线程涉及内核态与用户态切换

从对比可以看出,偏向锁在释放锁时的成本为零,这是其在无竞争场景下性能优异的关键原因。

五、偏向锁的竞争与撤销

虽然偏向锁在无竞争场景下性能优异,但当发生锁竞争时,需要进行偏向锁的撤销操作。偏向锁的撤销需要全局安全点(Safe Point),因为撤销操作涉及修改对象头的Mark Word,而其他线程可能正在使用该对象的锁状态。

5.1 撤销流程详解

5.2 为什么需要安全点?

安全点是JVM中的特定位置,此时所有线程的状态是确定的,JVM可以安全地进行内存管理、锁状态修改等操作。在安全点暂停线程的成本较高(需等待所有线程到达安全点),但偏向锁的撤销是罕见操作(仅在第一次竞争时发生),因此整体收益大于成本。

六、偏向锁的优化与权衡

6.1 偏向锁延迟初始化

JVM默认启动时有4秒的偏向锁延迟(-XX:BiasedLockingStartupDelay=0可关闭),因为JVM启动阶段会有大量类加载和静态初始化操作,可能触发不必要的锁竞争。

6.2 批量重偏向与撤销

当一个类的对象频繁发生偏向锁撤销时,JVM会认为该类不适合偏向锁,会批量将该类的对象置为不可偏向状态,避免频繁撤销带来的性能损耗。

6.3 禁用偏向锁

在明确知道锁会被多线程竞争的场景下(如线程池任务),可通过-XX:-UseBiasedLocking禁用偏向锁:

ExecutorService executor = Executors.newFixedThreadPool(10);

executor.submit(() -> {

synchronized (this) {

// 多线程竞争场景,禁用偏向锁可避免撤销开销

}

});

七、总结

偏向锁通过预测锁的使用模式,将无竞争场景下的锁获取和释放成本降为零,显著提升了单线程或无竞争场景下的性能。其释放无需任何操作的特性,是通过"状态持久化"设计实现的,即锁的偏向状态会一直保持,直到发生竞争。

虽然偏向锁的撤销需要全局安全点,成本较高,但由于撤销是罕见事件,整体性能收益远大于成本。理解偏向锁的设计原理和机制后,开发者可以在设计并发代码时,通过减少锁竞争来充分利用偏向锁的优势,例如使用线程封闭、减少不必要的同步块、优先使用单线程处理模式等。

在实际应用中,应根据具体场景选择合适的锁机制。偏向锁适用于大多数单线程或无竞争的场景,而在竞争激烈的场景下,可能需要考虑使用其他锁机制(如轻量级锁或重量级锁)。通过合理选择和使用锁机制,可以有效提升Java应用的并发性能。

相关推荐

iPhone 来电闪光怎么开?iPhone 信息闪光/通知闪光灯设置教程!
江苏欧瑞达新材料科技有限公司:荣获江苏省瞪羚企业,开启发展新征程
教程:应用提交流程
365购物商城

教程:应用提交流程

📅 08-19 👁️ 1535
暗黑3宝石精华怎么获得
英国beat365官方APP

暗黑3宝石精华怎么获得

📅 07-11 👁️ 8801
手机订票软件排行榜TOP10推荐
英国beat365官方APP

手机订票软件排行榜TOP10推荐

📅 07-01 👁️ 4370
苹果官方合作商新联借力有赞启动新品预售,有赞同步推出“新品发售解决方案”
热血街篮最新新闻
英国beat365官方APP

热血街篮最新新闻

📅 07-18 👁️ 6426
临榆炸鸡腿真香定律:一口入魂的中式炸鸡,凭啥让网友疯狂打call?
阴阳师如何建立阴阳寮?
365bet下载手机版

阴阳师如何建立阴阳寮?

📅 07-10 👁️ 5163