Synchronized锁获取与升级流程——从偏向锁到重量级锁
synchronized 关键字是 Java 并发编程的元老,很多人对它的印象还停留在“重量级”、“性能差”。但从 JDK 1.6 开始,synchronized 引入了锁升级机制,使其变得非常智能。
这套机制的核心思想是“按需分配”,在无竞争或低竞争时提供极高的性能,只在竞争激烈时才退化为传统的重量级锁。它的升级路径如下,并且此过程不可逆(只能升级,不能降级):
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
1. 无锁状态 (No Lock)
一个对象在刚被创建时,其对象头的 Mark Word 中没有任何锁信息,此时它就处于无锁状态。
2. 偏向锁 (Biased Lock)
升级时机:当第一个线程访问同步代码块时,锁会升级为偏向锁。
核心机制:
通过 CAS (Compare-And-Swap) 操作,将当前线程的 ID 记录在对象头的 Mark Word 中。
此后,该线程再次进入或退出同步块时,不再需要进行任何 CAS 操作,只需简单检查 Mark Word 中的线程 ID 是否是自己。
目标场景:适用于只有一个线程会访问同步代码块的场景。这大大减少了加锁的开销,性能几乎等同于无锁。
3. 轻量级锁 (Lightweight Lock)
升级时机:当第二个线程尝试获取这个偏向锁时,偏向锁模式宣告结束,进入偏向锁撤销(Bias Revocation)流程,并升级为轻量级锁。
核心机制:
线程在自己的栈帧中创建一块名为 Lock Record 的空间,用于拷贝对象头的 Mark Word(Displaced Mark Word)。
通过 CAS 操作,尝试将对象头的 Mark Word 更新为指向这个 Lock Record 的指针。
如果 CAS 成功,则获取锁成功。如果失败,线程会进行自适应自旋(Adaptive Spinning),即在原地循环一小段时间,而不是立即挂起,期待锁能很快被释放。
目标场景:适用于线程交替执行同步代码块,且每次持有锁的时间很短的场景。自旋避免了线程挂起和唤醒带来的系统开销。
4. 重量级锁 (Heavyweight Lock)
升级时机:当轻量级锁的自旋失败(例如,超过自旋次数或有其他线程也在自旋),锁就会膨胀(Inflate)为重量级锁。
核心机制:
锁依赖于操作系统底层的 Mutex Lock(互斥量) 来实现。
未能获取锁的线程不再自旋,而是会被挂起(BLOCKED),并进入一个等待队列。
等待持有锁的线程释放锁后,再由操作系统唤醒其中一个等待的线程。
目标场景:适用于高并发、高竞争的场景。虽然上下文切换(用户态 ↔ 内核态)开销巨大,但它能保证在激烈竞争下,所有线程都能有序地获取锁。
总结
| 锁状态 | 核心机制 | 适用场景 | 开销 |
|---|---|---|---|
| 无锁 | - | 无线程竞争 | 无 |
| 偏向锁 | CAS 记录线程 ID,后续仅检查 | 单线程访问 | 极低 |
| 轻量级锁 | CAS + 自适应自旋 | 线程交替执行,竞争不激烈 | CPU 自旋开销 |
| 重量级锁 | 操作系统 Mutex,线程挂起/唤醒 | 高并发,竞争激烈 | 重量级,涉及内核态切换 |

synchronized 通过这套精巧的锁升级机制,在性能和线程安全之间取得了完美的平衡。它不再是那个“笨重”的锁,而是一个能够根据竞争情况自动调整策略的智能锁。
揭秘 Synchronized 的可重入性:它是如何实现的?
synchronized 的可重入性(Reentrancy)是其核心特性之一,它保证了同一个线程可以多次获取自己已经持有的锁,从而避免了自己把自己锁死(死锁)的情况。
1. 什么是可重入性?为什么它很重要?
想象以下场景:一个类中有两个 synchronized 方法,其中一个方法调用了另一个。
public class ReentrantExample {public synchronized void methodA() {System.out.println("Executing methodA...");methodB(); // 调用另一个同步方法}public synchronized void methodB() {System.out.println("Executing methodB...");}
}
如果没有可重入性,当一个线程执行 methodA() 时,它获取了对象的锁。接着,它尝试调用 methodB(),此时它需要再次获取同一个对象的锁。由于锁已经被自己持有,它会陷入无限等待,造成死锁。
可重入性解决了这个问题:它允许已经持有锁的线程,无需等待,直接再次获取该锁。
2. Synchronized 如何在不同锁状态下保证可重入?
synchronized 的可重入性设计贯穿了从偏向锁到重量级锁的整个升级过程,确保在任何状态下行为一致。
a) 偏向锁阶段 (Biased Lock)
这是实现最简单、开销最小的阶段。
机制:线程进入同步块时,只需检查对象头 Mark Word 中记录的线程 ID 是否是自己。
重入:如果是,直接进入,无需任何额外操作。这就像进自己家门,刷脸就行。
b) 轻量级锁阶段 (Lightweight Lock)
当锁升级为轻量级锁后,机制变得复杂一些。
机制:线程会在自己的栈帧中创建 Lock Record 来持有锁。
重入:当线程尝试重入时,它会发现对象头已经指向一个位于自己栈帧的 Lock Record。此时,JVM 会在当前栈帧中再创建一个 Lock Record,但将其内部的 displaced_header 字段设为 null。这个 null 标记就代表了一次重入。退出同步块时,每遇到一个 null 的 Lock Record,就代表完成一次重入解锁。
c) 重量级锁阶段 (Heavyweight Lock)
这是最经典、最广为人知的实现方式。
机制:锁由底层的 ObjectMonitor 对象管理。
重入:ObjectMonitor 内部有一个名为 _recursions 或 _count 的计数器。当一个线程获取锁后,计数器变为 1。该线程每重入一次,计数器就 +1。相应地,每退出一个同步块,计数器就 -1。直到计数器归零,锁才被真正释放。
3. 总结
synchronized 的可重入性是其内在基因,通过在不同锁状态下采用不同的策略(检查线程ID、设置null标记、维护计数器),无缝地保证了同一线程可以安全、高效地多次进入同步代码块。
这种精巧的设计,使得开发者可以放心地在同步方法中调用其他同步方法,而不必担心死锁问题,极大地增强了 synchronized 关键字的健壮性和易用性。
为什么说 Synchronized 是一个“不公平”的锁?
在面试或技术讨论中,我们常听到一个结论:synchronized 是一个非公平锁(Non-fair Lock)。
这个“不公平”听起来像个缺点,但实际上,它是 synchronized 为了性能而做出的一个精明权衡。本文将带你彻底搞懂其中的缘由。
1. 首先,什么是公平与非公平?
让我们用一个生活中的例子来理解:
公平锁 (Fair Lock):就像在银行排队办业务,讲究“先来后到”。排在队首的客户(线程)一定比队尾的客户先得到服务。它保证了所有等待的线程最终都能获得锁,不会“饿死”。
非公平锁 (Non-fair Lock):不遵循严格的排队规则。当锁被释放时,系统允许一个新来的、尚未排队的线程去“插队”,直接尝试获取锁。如果它成功了,就跳过了所有正在排队的线程。
synchronized 就属于后者。
2. Synchronized 的“不公平”体现在哪里?
synchronized 的非公平性贯穿于其锁机制中,尤其在从轻量级锁升级到重量级锁的过程中表现得淋漓尽致。
a) 轻量级锁阶段
当一个线程释放轻量级锁时,其他正在自旋等待的线程会通过 CAS 竞争锁。这个竞争本身就是“无序”的,谁的 CAS 操作先成功,谁就获得锁,没有排队的概念。
b) 重量级锁阶段
这是非公平性最核心的体现。当锁膨胀为重量级锁后,所有获取不到锁的线程都会被挂起,放入一个等待队列(_EntryList)中。
当持有锁的线程释放锁时,JVM 面临一个选择:
- 公平的做法:从等待队列的头部唤醒一个线程,让它获取锁。
- 非公平的做法:允许一个刚刚进入同步块、尚未被挂起的新线程,直接尝试获取锁。
HotSpot 虚拟机的synchronized** 选择了后者**。 它会先让新来的线程尝试“抢”一下锁,如果抢不到,这个新线程才会被挂起并加入等待队列的队尾。
3. 为什么要设计成“不公平”?—— 性能!
不公平的设计看似“不道德”,但它背后是极致的性能追求,核心目标是提高系统的总吞吐量 (Throughput)。
这主要是为了避免不必要的上下文切换 (Context Switch)。
- 线程唤醒的成本:唤醒一个被挂起的线程成本非常高。它需要从内核态切换回用户态,恢复线程的运行环境,这比一个正在运行的线程执行几百条指令还要慢。
- 非公平的优势:如果一个新来的、正在 CPU 上运行的线程能够立即获取锁并完成任务,就避免了“唤醒旧线程”和“挂起新线程”这两次昂贵的上下文切换。虽然对排队的线程不公平,但从整个系统的角度看,减少了两次线程状态转换,总的执行效率更高。
简单来说,系统认为,让一个“热乎”的(正在运行的)线程直接干活,比费劲去叫醒一个“睡着”的(被挂起)线程,成本要低得多。
4. 非公平的代价:线程饿死 (Starvation)
非公平锁的潜在风险是线程饿死。理论上,如果运气极差,一个排队的线程可能会一直被新来的线程“插队”,导致它永远也获取不到锁。
不过,在实际应用中,这种情况发生的概率极低。synchronized 的设计是在绝大多数场景下,用极小的“饿死”风险换取显著的性能提升。
5. 如果我需要公平锁怎么办?
如果你有必须保证公平性的业务场景(例如,资源需要严格按申请顺序分配),可以使用 java.util.concurrent.locks.ReentrantLock。
它提供了一个构造函数来让你明确选择:
// 默认构造函数,创建一个非公平锁 (性能更高)
Lock nonFairLock = new ReentrantLock();// 传入 true,创建一个公平锁 (保证顺序,牺牲部分性能)
Lock fairLock = new ReentrantLock(true);
总结
synchronized 被设计为非公平锁,并非一个缺陷,而是一种为性能优化的策略。它通过允许新线程“插队”,最大限度地减少了昂贵的线程上下文切换,从而提高了系统的整体吞吐量。
这是一个经典的性能与公平性之间的权衡 (Trade-off)。对于绝大多数应用场景,synchronized 的这种“不公平”所带来的性能优势,远比其微乎其微的“饿死”风险更有价值。
