Java中的偏向锁、轻量级锁与重量级锁详解
字数 1812 2025-11-25 13:14:27
Java中的偏向锁、轻量级锁与重量级锁详解
在Java多线程编程中,synchronized关键字用于实现线程同步,确保共享资源的安全访问。为了优化synchronized的性能,JVM引入了锁升级机制,主要包括偏向锁、轻量级锁和重量级锁。下面我将详细解释这三种锁的概念、升级过程及其原理。
1. 锁升级的背景
- 问题:早期的synchronized直接使用操作系统级别的互斥锁(重量级锁),涉及用户态到内核态的切换,性能开销大。
- 优化思路:统计发现,大部分多线程场景下,锁竞争并不激烈。因此,JVM采用"渐进式"策略,根据实际竞争情况动态升级锁,减少不必要的开销。
2. 对象头与锁状态
- 对象头结构:每个Java对象在内存中分为对象头、实例数据和对齐填充。对象头包含Mark Word(存储哈希码、GC年龄、锁标志等)和类型指针。
- Mark Word的锁状态标志:
- 无锁状态:锁标志位为"01",是否偏向锁为"0"。
- 偏向锁:锁标志位为"01",是否偏向锁为"1"。
- 轻量级锁:锁标志位为"00"。
- 重量级锁:锁标志位为"10"。
- GC标记:锁标志位为"11"。
3. 偏向锁(Biased Locking)
- 设计目标:在无竞争或只有一个线程访问同步块时,消除同步开销。
- 工作原理:
- 加锁过程:当线程首次访问同步块时,通过CAS操作将对象头中的偏向锁标志置为"1",并记录线程ID。之后该线程再次进入时,无需CAS操作,直接检查线程ID是否匹配。
- 撤销条件:当其他线程尝试竞争锁时,持有偏向锁的线程需要撤销偏向锁。若原线程仍存活,则升级为轻量级锁;若已不活动,则直接转为无锁状态或重新偏向。
- 适用场景:单线程重复访问同步代码块。
4. 轻量级锁(Lightweight Locking)
- 设计目标:当多个线程交替执行同步块(无实际竞争),避免直接使用重量级锁。
- 工作原理:
- 加锁过程:
- 在代码进入同步块时,若对象处于无锁状态,JVM在当前线程的栈帧中创建锁记录(Lock Record),并复制对象头的Mark Word到锁记录中。
- 然后通过CAS操作将对象头的Mark Word替换为指向锁记录的指针。若成功,则获取轻量级锁;若失败,表示存在竞争,进入自旋优化。
- 解锁过程:使用CAS将锁记录中的Mark Word替换回对象头。若成功,则无竞争;若失败,表示锁已升级,需要释放锁并唤醒等待线程。
- 加锁过程:
- 自旋优化:竞争线程不会立即阻塞,而是通过循环(自旋)尝试获取锁,避免线程切换开销。自旋次数由JVM自适应调整(如基于前一次自旋成功次数)。
- 升级条件:自旋超过阈值或有多线程竞争时,升级为重量级锁。
5. 重量级锁(Heavyweight Locking)
- 设计目标:处理高竞争场景,通过操作系统互斥量(Mutex)实现线程阻塞和唤醒。
- 工作原理:
- 锁对象指向操作系统级的互斥量,未获取锁的线程会被加入等待队列,进入阻塞状态(线程切换开销大)。
- 依赖内核的同步机制,如Linux下的futex。
- 适用场景:多线程激烈竞争锁资源。
6. 锁升级的全过程
- 初始状态:对象为无锁状态(偏向锁禁用或未启用)。
- 偏向锁启用:当线程A首次访问同步块,启用偏向锁并记录线程ID。
- 偏向锁撤销:线程B尝试竞争时,若线程A仍活动,暂停线程A,检查其是否持有锁。若持有,升级为轻量级锁;否则,撤销偏向锁后重新竞争。
- 轻量级锁竞争:线程B通过自旋尝试获取锁。若自旋成功,线程B获得锁;若自旋失败(如超时或线程C加入竞争),升级为重量级锁。
- 重量级锁阻塞:线程B和C进入阻塞队列,由操作系统调度。
7. 锁的优缺点对比
- 偏向锁:优点是无竞争时开销极小;缺点是撤销需要STW(Stop-The-World),可能增加延迟。
- 轻量级锁:优点是线程交替执行时无需阻塞;缺点是自旋会消耗CPU资源。
- 重量级锁:优点是保证公平性和稳定性;缺点是线程切换开销大。
8. 实践建议
- 高并发场景(如秒杀系统)可禁用偏向锁(-XX:-UseBiasedLocking),避免撤销开销。
- 轻量级锁的自旋次数可通过-XX:PreBlockSpin调整,但JDK 1.6后已优化为自适应自旋。
通过锁升级机制,JVM在保证线程安全的同时,显著提升了synchronized的性能,使其成为Java高并发编程的核心工具之一。