在高并发编程领域,线程安全是一个绕不开的话题。Java 通过 synchronized 关键字为我们提供了一种简单易用的内置锁机制,用于保证共享数据在多线程环境下的原子性、可见性和有序性。然而,凡事有利必有弊,锁在带来数据安全的同时,也可能成为性能的瓶颈。
正如开发手册所强调的:
【强制】 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明: 尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
这背后的核心思想,就是在安全与性能之间寻找最佳平衡点。
一、Synchronized 的前世今生:从重量级锁到智能优化
在 Java 的早期版本(JDK 1.5 之前),synchronized 的实现非常“重”。它直接依赖于操作系统底层的互斥量(Mutex Lock)来实现。这意味着,每当线程需要获取或释放锁时,都需要在用户态(User Mode)和内核态(Kernel Mode)之间进行切换。

这种切换的成本是相当高昂的。操作系统需要保存当前用户线程的上下文信息(如寄存器值、变量等),然后加载内核的上下文来执行线程的阻塞或唤醒操作,完成后再切换回来。如果同步代码块本身执行得非常快,那么线程切换的开销甚至可能超过代码执行本身的开销,导致性能急剧下降。因此,早期的 synchronized 被称为重量级锁。
为了改善这一状况,从 JDK 1.6 开始,Java 虚拟机(JVM)对 synchronized 进行了大刀阔斧的优化,引入了偏向锁(Biased Locking) 和轻量级锁(Lightweight Locking) 。其核心思想是:锁的竞争程度是动态变化的,我们应该根据实际情况逐步升级锁的级别,而不是一开始就动用最“重”的武器。 这种锁状态的升级过程,就是我们常说的锁膨胀(Lock Inflation) 。
二、锁的秘密:对象头中的 Mark Word
synchronized 锁的实现并不依赖于某个独立的锁对象,而是直接将锁信息记录在了 Java 对象头里。每个 Java 对象都由对象头(Header)和实例数据(Instance Data)组成。对象头中包含一个名为 Mark Word 的区域,它就是实现偏向锁、轻量级锁和重量级锁的关键。
Mark Word 的结构在 64 位 JVM 中通常为 8 字节(64 位),它是一个“多功能”的数据结构,其内容会随着锁状态的改变而改变,实现了空间的复用。

从上图可以看出,Mark Word 通过末尾的几位标志位来区分当前对象处于哪种锁状态:
- 无锁状态(标志位 01): 存储对象的哈希码(HashCode)、GC 分代年龄等信息。
- 偏向锁状态(标志位 01,但偏向锁位置 1): 存储偏向的线程 ID、Epoch 等信息。
- 轻量级锁状态(标志位 00): 存储一个指向线程栈中锁记录(Lock Record) 的指针。
- 重量级锁状态(标志位 10): 存储一个指向监视器(Monitor) 对象的指针。

三、重量级锁的底层基石:Monitor
在深入了解锁升级之前,我们必须先理解重量级锁的底层原理——Monitor,也常被称为“管程”或“监视器锁”。
每个 Java 对象天生就与一个 Monitor 关联。你可以将 Monitor 理解为一个同步工具或机制。当一个对象被 synchronized 锁定时,实际上就是获取了这个对象关联的 Monitor 的所有权。
Monitor 的本质是由 C++ 的 ObjectMonitor 类实现的,其核心依赖于操作系统的 Mutex Lock。这就是为什么重量级锁需要进行用户态到内核态切换的根本原因。
// hotspot/src/share/vm/runtime/objectMonitor.hpp
class ObjectMonitor {
// ...
volatile markOop _header; // 锁对象的Mark Word
void* _owner; // 指向持有锁的线程
WaitSet* _WaitSet; // 调用了wait()方法的线程被阻塞在此
EntryList* _EntryList; // 等待获取锁的线程被阻塞在此
// ...
};
```
当多个线程同时竞争一个锁时:
- 成功获取锁的线程成为 Monitor 的
_owner。 - 获取失败的线程会被封装成节点,放入
_EntryList队列中挂起,等待被唤醒。 - 这个挂起和唤醒的过程,就需要操作系统介入,因此开销巨大。
总结一下 Monitor、Java 对象和线程的关系:
- 当一个对象升级为重量级锁时,其 Mark Word 会变成一个指针,指向与之关联的 Monitor 对象。
- Monitor 的
_owner字段会记录当前持有该锁的线程 ID。


了解了 synchronized 的演进背景、Mark Word 的结构以及重量级锁的底层实现后,我们就为理解锁升级铺平了道路。接下来,我们将由浅入深,详细剖析从无锁到偏向锁、轻量级锁,最终膨胀为重量级锁的全过程。
好的,我们继续。现在我们将深入探讨 synchronized 优化的第一级阶梯——偏向锁。
四、第一级优化:偏向锁 (Biased Locking) - 为“老顾客”开设的VIP通道
HotSpot 虚拟机的开发者们经过研究发现,在绝大多数情况下,一个锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。为了让这种情况下的性能开销降到最低,偏向锁应运而生。
偏向锁,顾名思义,就是“偏心”的锁,它会偏向于第一个获得它的线程。
可以把它想象成一个私人停车位:
第一个开车来的线程(比如线程 A)把车停好,并在车位上挂上自己的名牌(在 Mark Word 中记录线程 ID)。只要没有其他人来抢这个车位,线程 A 之后每次来,看到是自己的名牌,就可以直接停车,无需再办理任何手续(无需 CAS 同步)。
这个“无需办理手续”的过程,就是偏向锁性能提升的关键。在没有竞争的情况下,它甚至消除了 CAS 操作,几乎没有同步开销。
偏向锁的获取流程
初始状态:一个对象刚被创建时,处于“可偏向”的无锁状态(标志位
01,但偏向锁位为0)。
首次加锁:当线程 A 第一次进入该对象的
synchronized 代码块时,JVM 会通过 CAS 操作 尝试将该线程的 ID 记录到对象头的 Mark Word 中,并将偏向锁标志位置为1。- 成功:线程 A 获得偏向锁。此时 Mark Word 中存储的是偏向线程的 ID。
- 失败:说明发生了竞争(可能其他线程已经持有了偏向锁,或正在升级),进入后续的锁升级流程。

后续访问:当持有偏向锁的线程 A 再次进入同步块时,它会检查 Mark Word 中存储的线程 ID 是否是自己的 ID。
- 是:表明它就是这位“老顾客”,可以直接进入同步代码块,无需任何额外的同步操作。
- 不是:说明锁对象偏向了其他线程,发生了竞争。
偏向锁的撤销与升级:当“新顾客”到来时
偏向锁的一个重要特点是:持有偏向锁的线程不会主动释放锁。只有当其他线程尝试获取该锁时,偏向锁才会被撤销(Revoke) 。
这个撤销过程需要在一个全局安全点(Global Safepoint) 进行,即一个所有线程都暂停执行的时刻。此时,JVM 会根据持有偏向锁的线程(我们称之为线程 A)的状态,决定下一步怎么走:
情况一:线程 A 还在同步代码块内
- 其他线程(线程 B)来竞争锁。
- JVM 发现线程 A 仍然活跃且持有锁。
- 此时,偏向锁会被撤销,并直接升级为轻量级锁。
- 线程 A 的栈帧中会创建锁记录(Lock Record),对象头的 Mark Word 会指向这个锁记录。线程 A 继续执行。
- 线程 B 则进入自旋状态,尝试获取这个新的轻量级锁。
情况二:线程 A 已经退出了同步代码块
- 其他线程(线程 B)来竞争锁。
- JVM 发现线程 A 已经不再持有该锁。
- 这时,锁对象会恢复到无锁状态(或者叫“匿名偏向”状态),其 Mark Word 会被重置。
- 然后线程 B 可以重新尝试获取锁,并可能将该锁偏向于自己。

解惑:偏向锁有竞争后,一定会变成轻量级锁吗?
这正是上面分析的关键点。答案是:不一定。
- 如果竞争发生时,原持有者还在同步块内,那么锁会升级为轻量级锁。
- 如果竞争发生时,原持有者已经退出了同步块,那么锁会恢复为无锁状态,让新的竞争者去偏向。
偏向锁与 hashCode() 的“恩怨”
在无锁状态下,Mark Word 的一部分空间是用来存储对象的 identity hash code 的。而一旦对象进入偏向锁状态,这部分空间就会被用来存储线程 ID。这就导致了一个重要的冲突:
一个已经计算过
identity hash code 的对象,无法再进入偏向锁状态。 JVM 会直接跳过偏向锁,在加锁时进入轻量级锁状态。如果一个对象正处于偏向锁状态,此时收到了计算其
identity hash code 的请求(即调用了System.identityHashCode() 或对象的hashCode()方法),会发生什么?- 偏向锁会立即被撤销。并且,锁会膨胀为重量级锁。
- 为什么直接膨胀为重量级锁,而不是轻量级锁? 因为轻量级锁的 Mark Word 需要指向栈上的锁记录,仍然没有空间存放
hashCode。而重量级锁的实现ObjectMonitor 中有专门的字段来存放这些信息。为了“安放”这个hashCode,JVM 选择直接升级到最完备的重量级锁状态。
偏向锁的现状与未来
偏向锁在 JDK 1.6 中被默认开启,但它有一个启动延迟。可以通过 JVM 参数进行配置:
-
XX:+UseBiasedLocking:开启偏向锁(默认开启)。 -
XX:BiasedLockingStartupDelay=0:关闭启动延迟,让程序启动后立即启用偏向锁。 -
XX:-UseBiasedLocking:关闭偏向锁,程序将直接进入轻量级锁状态。
然而,随着技术的发展,偏向锁带来的维护成本(尤其是在全局安全点撤销的复杂性)在某些场景下已经超过了其带来的性能收益。因此,从 JDK 15 开始,偏向锁已被默认禁用并被标记为废弃(JEP 374) 。

这意味着,在未来的 Java 版本中,我们将告别偏向锁。但在理解 synchronized 的锁升级历史中,它仍然是不可或缺的一环。
好的,我们继续博客的第三部分,深入探讨 synchronized 优化的第二级阶梯——轻量级锁。
五、第二级优化:轻量级锁 (Lightweight Locking) - 自旋不息,避免阻塞
当偏向锁的“一人独享”模式被打破,即多个线程开始交替访问同一个同步块时,锁就会升级为轻量级锁。轻量级锁的核心设计思想是:线程之间的竞争和锁占用时间通常非常短暂。与其让线程一上来就进入阻塞状态(这需要昂贵的内核态切换),不如让它“稍等片刻”,进行几次空循环(即自旋),看看持有锁的线程是否会很快释放锁。
可以把轻量级锁想象成一个办公室里的空闲会议室:
大家都遵循一个君子协定。当线程 A 想用会议室时,它会在门口挂上“使用中”的牌子(通过 CAS 修改 Mark Word)。如果线程 B 也想用,它看到牌子后不会立刻走开去排队(阻塞),而是在门口踱步观望一会儿(自旋),因为它相信线程 A 很快就会出来。
这种“自旋”机制,就是轻量级锁避免性能损耗的关键。只要竞争不激烈,线程通过自旋就能成功获取锁,从而避免了重量级锁的系统调用开销。
轻量级锁的加锁过程
当一个线程尝试获取一个已经被撤销了偏向锁的对象时,或者一开始就关闭了偏向锁时,就会进入轻量级锁的加锁流程:
创建锁记录(Lock Record) :JVM 会在当前线程的栈帧(Stack Frame)中开辟一块名为“锁记录”的空间,用于存放锁对象当前的 Mark Word 的一个拷贝。这被称为 Displaced Mark Word。
CAS 替换 Mark Word:线程尝试使用 CAS(Compare-And-Swap) 原子操作,将对象头的 Mark Word 更新为一个指向该锁记录的指针,并将锁标志位改为
00。- 成功:恭喜!当前线程成功获取了轻量级锁。此时,对象头的 Mark Word 指向了当前线程栈中的锁记录,表明该对象已被锁定。
- 失败:表示在它操作的瞬间,已经有其他线程抢先一步获取了锁。这时,对象头的 Mark Word 已经指向了其他线程的锁记录。
自旋等待:获取锁失败的线程并不会立即阻塞,而是进入自旋状态,循环地尝试通过 CAS 获取锁。

自旋锁的优化:从固定到自适应
早期的 JVM 中,自旋的次数是固定的(例如 10 次),或者由一个复杂的公式决定。这种“一刀切”的方式并不智能。
从 JDK 6 开始,引入了自适应自旋锁(Adaptive Spinning) 。它的原理非常人性化:
- 如果一个线程在某次自旋后成功获取了锁,JVM 就会认为下一次它也很可能成功,因此会允许它在未来进行更长时间的自旋。
- 反之,如果一个线程很少能通过自旋成功获取锁,JVM 就会认为继续自旋只是在浪费 CPU 资源,因此会减少甚至直接跳过下一次的自旋过程,直接进入锁膨胀阶段。
轻量级锁的释放过程
释放锁的过程同样基于 CAS 操作:
线程使用 CAS 尝试将之前保存在锁记录(Displaced Mark Word)中的数据恢复到对象头的 Mark Word 中。
- 成功:说明在持有锁期间没有其他线程发生过激烈的竞争(没有导致锁膨胀),锁被成功释放,状态恢复到无锁。
- 失败:这通常意味着一个严重的情况——在持有锁的期间,有其他线程在自旋等待,并且因为等待时间过长或竞争线程过多,已经将这个锁升级为了重量级锁。此时,对象头的 Mark Word 已经被修改为指向一个重量级锁的 Monitor。
如果 CAS 恢复失败,释放锁的线程会进入重量级锁的释放流程,即唤醒在 Monitor 的
_EntryList中等待的线程。
偏向锁与轻量级锁的核心区别
- 释放时机:偏向锁只有在遇到竞争时才会被动释放;轻量级锁每次退出同步块时都必须主动释放。
- 竞争处理:偏向锁遇到竞争直接升级;轻量级锁遇到竞争会先自旋尝试,自旋失败后才会升级。
锁升级:从轻量级到重量级
轻量级锁并非万能。如果自旋的线程长时间无法获取锁,或者自旋的线程数量越来越多,自旋本身就会成为一种 CPU 资源的浪费。此时,就会发生最后一次锁升级:
- 锁膨胀为重量级锁,锁标志位变为
10。 - 对象头的 Mark Word 不再指向线程栈,而是指向与之关联的 Monitor 对象。
- 所有等待锁的线程(包括正在自旋的线程)都会被挂起,进入 Monitor 的
_EntryList中排队等待,彻底告别 CPU 空转。
六、最终形态:重量級锁 (Heavyweight Locking)
当轻量级锁的自旋也无法解决激烈的竞争时,synchronized 就会亮出它的最后底牌——重量级锁。此时,锁不再依赖于线程的“自觉”自旋,而是由操作系统来强制执行“排队”规则,确保公平和秩序。
我们回到最初讨论的 Monitor。当锁膨胀为重量级锁后:
- 对象的 Mark Word 会被修改为一个指针,指向一个重量级的 Monitor 对象。
- 所有未能获取锁的线程,都会被阻塞,并放入 Monitor 内部的
_EntryList队列中等待。 - 当持有锁的线程执行完同步代码块,它会释放锁并唤醒
_EntryList中的一个或多个等待线程,由操作系统调度其中一个线程来获取锁。
这个过程在字节码层面由 monitorenter 和 monitorexit 两条指令来控制。monitorenter 尝试获取 Monitor 的所有权,而 monitorexit 则释放它。

重量级锁虽然性能开销最大(因为涉及线程的阻塞和唤醒),但它能够处理任何激烈的并发场景,是保障线程安全的最后一道防线。
七、锁升级之谜:消失的哈希码和 GC 年龄去哪了?
这是一个非常有趣且深入的技术细节。我们知道,在无锁状态下,Mark Word 存储了对象的 identity hash code 和 GC 分代年龄。但在锁升级后,Mark Word 的空间被线程 ID、锁记录指针或 Monitor 指针占用了,那么这些原始信息被移到哪里去了呢?
偏向锁状态:
- Mark Word 被线程 ID 覆盖。此时没有地方存储
hashCode。 - 这就是为什么一个已经计算过
hashCode 的对象无法进入偏向锁状态的原因。 - 反之,如果一个对象处于偏向锁状态时,收到了计算
hashCode 的请求,它会立即撤销偏向锁并膨胀为重量级锁,将hashCode存入 Monitor 中。
- Mark Word 被线程 ID 覆盖。此时没有地方存储
轻量级锁状态:
- 在当前线程的栈帧中创建的锁记录(Lock Record) 里,有一份对象 Mark Word 的拷贝(Displaced Mark Word)。
- 对象的
hashCode 和 GC 年龄等信息就保存在这份拷贝中。 - 当锁释放时,会通过 CAS 操作将这些信息写回到对象头。
重量级锁状态:
- Mark Word 指向了 Monitor 对象。
- Monitor 对象的内部有专门的字段,可以记录和保存对象在非锁定状态下的整个 Mark Word 信息。
- 当锁最终被释放后,这些信息同样会被恢复到对象头中。
八、三种锁策略的优缺点对比
| 锁类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 加解锁几乎没有额外开销,性能接近非同步代码。 | 如果存在锁竞争,会带来额外的锁撤销开销。 | 只有一个线程会访问同步代码块的场景。 |
| 轻量级锁 | 线程不阻塞,响应速度快。 | 如果长时间获取不到锁,自旋会持续消耗 CPU 资源。 | 追求响应时间,且同步代码块执行速度非常快的场景。 |
| 重量级锁 | 线程竞争不使用自旋,不空耗 CPU。 | 线程阻塞和唤醒的开销大,响应时间慢。 | 追求吞吐量,且同步代码块执行时间较长的场景。 |
九、JIT 编译器的魔法:超越锁升级的优化
除了锁升级,现代 JVM 的即时(JIT)编译器还会进行一些更智能的锁优化,进一步提升性能。
1. 锁消除(Lock Elimination)
JIT 编译器在动态编译时,会通过逃逸分析来判断一个锁对象是否真的被多个线程共享。如果它发现某个锁对象(如 new Object())只在当前方法内部使用,从未“逃逸”出去被其他线程访问,那么这个锁就是不必要的。
public void someMethod() {
Object lock = new Object();
// JIT会发现lock对象从未被其他线程访问
// 因此,这个synchronized会被“消除”,编译后的代码中将没有加锁操作
synchronized (lock) {
// ... do something
}
}从 JIT 的角度看,这等同于无视 synchronized 关键字,直接执行内部代码,从而消除了加锁和解锁的开销。
2. 锁粗化(Lock Coarsening)
如果 JIT 编译器发现一系列连续的操作都对同一个对象反复加锁和解锁,它会认为这种频繁的操作是不必要的。此时,它会将多个相邻的同步块合并成一个更大的同步块,这就是锁粗化。
public void anotherMethod() {
StringBuffer sb = new StringBuffer();
// 原始代码:频繁加锁解锁
synchronized (sb) {
sb.append("Hello");
}
synchronized (sb) {
sb.append(" World");
}
// JIT优化后的等效代码:锁粗化
// synchronized (sb) {
// sb.append("Hello");
// sb.append(" World");
// }
}通过一次加锁和一次解锁来覆盖整个操作序列,避免了多次申请和释放锁的性能损耗,提升了整体性能。
十、结论:Synchronized 的进化之旅
synchronized 的锁升级过程,一言以蔽之,就是 JVM 遵循 “先自旋,不行再阻塞” 的智能策略。
从 JDK 1.6 开始,synchronized 早已不是那个曾经被诟病为“性能低下”的重量级锁。它通过引入偏向锁和轻量级锁,将一个悲观锁在特定场景下转变为乐观锁的实现,极大地提升了性能。
- 单线程场景:使用偏向锁,几乎零开销。
- 竞争不激烈场景:升级为轻量级锁,通过自旋避免阻塞,保证响应速度。
- 竞争激烈场景:膨胀为重量级锁,通过操作系统调度保证公平性,充分利用吞吐量。
再加上 JIT 编译器在更高维度的锁消除和锁粗化优化,使得 synchronized 成为一个既强大又智能的同步工具。理解其底层的演进逻辑,不仅能帮助我们写出更高效的并发代码,更能让我们体会到现代虚拟机设计的精妙与智慧。
Synchronized 高频面试题
第一部分:基础篇
1. 谈谈你对 synchronized 的理解?
总:synchronized 是 Java 中的一个关键字,它是一种内置的、非公平的、可重入的锁,主要用于解决多线程环境下的数据安全问题,保证了代码的原子性、可见性和有序性。
分:
- 原子性:被
synchronized 修饰的代码块在执行期间不可被中断。 - 可见性:一个线程对共享变量的修改,能够立即被其他线程看到。这是通过在解锁时刷新线程本地内存到主内存,加锁时清空本地内存来实现的。
- 有序性:可以防止指令重排序,保证代码按书写顺序执行。
2. synchronized 有哪几种用法?锁的是什么?有什么区别?
总:主要有三种用法,其核心区别在于锁的范围(或称锁的粒度)不同,这直接决定了程序的并发能力。
分:
修饰实例方法 (实例锁):
- 锁对象:
this,即当前实例对象。 - 区别与范围:锁只对当前这一个实例有效。多个实例之间有各自的锁,互不影响,并发度较高。
- 锁对象:
修饰静态方法 (类锁):
- 锁对象:
MyClass.class,即当前类的 Class 对象。 - 区别与范围:锁是全局的,作用于这个类的所有实例。一个线程拿到类锁,会阻塞其他线程访问该类的任何静态同步方法。并发度最低。
- 锁对象:
修饰代码块:
- 锁对象:括号里指定的对象。
- 区别与范围:最灵活,性能最好。因为它允许我们只锁住必要的代码(减小锁粒度),而不是锁定整个方法,从而显著提高并发能力。这是最推荐的用法。
3. synchronized 是可重入锁吗?在各种锁状态下是如何实现的?
总:是的,它是可重入锁。并且在偏向锁、轻量级锁和重量级锁三种状态下,都支持可重入,只是实现机制不同。
分:
偏向锁状态:实现最简单。线程进入时,只需检查 Mark Word 中的线程 ID 是否是自己。如果是,就直接进入,无需任何额外操作。
轻量级锁状态:通过线程栈上的锁记录(Lock Record) 实现。线程会检查对象 Mark Word 指向的锁记录是否位于自己的栈上。如果是,说明是重入,此时会在栈中压入一个特殊的空标记来计数,退出时移除标记即可。
重量级锁状态:通过底层的 Monitor 实现。Monitor 内部有两个关键字段:
_owner 指向持有锁的线程,_recursions 是一个重入计数器。- 线程首次获取锁,
_owner 指向它,_recursions计为 1。 - 线程再次重入,发现
_owner 是自己,就将_recursions的计数值加 1。 - 退出同步块时,
_recursions减 1。当减到 0 时,线程才真正释放锁。
- 线程首次获取锁,
第二部分:进阶篇 - 锁升级
4. 为什么说 synchronized 在 JDK 1.6 后性能得到了巨大提升?
总:因为它引入了锁升级机制,避免了在所有情况下都使用重量级锁。
分:
- 优化前:早期版本直接依赖操作系统的 Mutex Lock,是纯粹的重量级锁。每次加锁解锁都涉及用户态和内核态的切换,成本极高。
- 优化后:引入了偏向锁和轻量级锁。JVM 会根据锁的竞争情况,沿着 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 的路径进行升级,这个过程叫锁膨胀。大部分情况下,锁竞争并不激烈,用偏向锁和轻量级锁就能解决,避免了重量级锁的性能开销。
5. 能简述一下锁升级的过程吗?
总:就是根据竞争的激烈程度,从偏向锁一步步升级到重量级锁的过程。
分:
偏向锁:
- 特点:只有一个线程访问,锁会“偏向”该线程。Mark Word 记录其线程 ID。
- 升级时机:当第二个线程来竞争时,偏向锁就会被撤销,并升级为轻量级锁。
轻量级锁:
- 特点:多个线程交替访问,但没有激烈冲突。线程通过 CAS 尝试获取锁,失败后会自旋(空转等待),而不是立即阻塞。
- 升级时机:如果一个线程自旋了很久还没拿到锁,或者同时自旋的线程太多,JVM 认为竞争很激烈,就会升级为重量级锁。
重量级锁:
- 特点:竞争非常激烈。所有获取不到锁的线程都会进入阻塞状态,等待操作系统唤醒。
- 实现:依赖操作系统的 Mutex Lock,通过底层的 Monitor 机制来管理线程排队。
第三部分:高级篇 - 原理与对比
6. Monitor 中的 _recursions 和 _counter 有什么区别?
总:它们的作用完全不同:_recursions 是为锁的持有者服务的,用于实现可重入;而 _counter 是为等待者服务的,记录有多少线程在排队。
分:
_recursions:重入次数计数器。- 谁用:只对当前持有锁的线程(Owner) 有效。
- 何时变:该线程每重入一次,值加 1;每退出一次,值减 1。
_counter:等待线程计数器(在ObjectMonitor 中,常与_EntryList关联)。- 谁用:记录有多少个线程在排队等待获取锁。
- 何时变:一个新线程来竞争锁失败被挂起时,值可能增加;一个线程被唤醒时,值可能减少。
7. synchronized 和 ReentrantLock 有什么区别?怎么选?
总:synchronized 是 JVM 内置关键字,使用简单且能自动释放锁;ReentrantLock 是 API 层面的类,功能更强大,但需要手动释放锁。
分:
| 对比维度 | synchronized (关键字) | ReentrantLock (类) |
|---|---|---|
| 用法 | 简单,JVM 自动管理 | 灵活,需在 finally 中手动 unlock() |
| 公平性 | 非公平 | 默认非公平,可设置为公平 |
| 高级功能 | 无 | 有: 1. 可中断等待 ( lockInterruptibly)2. 可超时等待 ( tryLock)3. 可绑定多个 Condition 实现精准唤醒 |
| 性能 | JDK 1.6 后与 ReentrantLock 差距不大 | 结合高级功能使用,可控性更强 |
选择建议:
- 首选
synchronized:在锁竞争不激烈、功能简单的场景下,代码更简洁,不易出错。 - 使用
ReentrantLock:当需要公平锁、可中断/超时等待、或多条件唤醒等高级功能时。