锁机制
synchronized 和 ReentrantLock 区别?
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层 | JVM 内置关键字 | Java 代码(AQS) |
| 锁的释放 | 自动释放(出块/异常) | 必须手动 unlock(),需 finally |
| 可中断等待 | 不支持 | lockInterruptibly() |
| 超时获锁 | 不支持 | tryLock(timeout) |
| 公平锁 | 不支持(非公平) | 构造器传 true 开启 |
| 条件变量 | 只有 wait/notify | 多个 Condition,精确唤醒 |
| 性能 | JDK 6+ 锁升级后差距小 | 高竞争下稍优 |
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须放 finally!
}简单场景用 synchronized(代码简洁,JVM 自动优化);需要超时/可中断/多条件时用 ReentrantLock。
ReentrantLock 怎么实现公平锁?
构造器传入 true 即可开启公平锁。区别在于:线程尝试获取锁时,会先检查等待队列里是否有排队的线程。
非公平锁(默认)
新线程直接 CAS 抢锁,不看队列。可能插队成功,吞吐量更高,但可能导致队列中的线程长期等待(饥饿)

公平锁
新线程先检查队列,若有等待线程则乖乖排队。严格按 FIFO 顺序,无饥饿,但线程切换开销大,吞吐量低

new ReentrantLock(); // 非公平锁(默认,性能更好)
new ReentrantLock(true); // 公平锁(按等待顺序获锁)什么是可重入锁?
同一个线程在持有锁的情况下,可以再次获取同一把锁而不会死锁,就叫可重入锁。
实现原理
锁内部维护一个持有线程和重入计数器。加锁时:若是同一线程,计数器 +1;释放时计数器 -1,归零才真正释放锁
为什么必要
synchronized 方法调用另一个 synchronized 方法(同一对象),若锁不可重入就会死锁。Java 的 synchronized 和 ReentrantLock 都是可重入的
public synchronized void methodA() {
methodB(); // 同一线程再次获取 this 的锁
// 可重入:计数器 2 → 不会死锁
}
public synchronized void methodB() {
// 计数器在此处 = 2
}
// methodA 返回时计数器从 2→1→0 真正释放ReentrantLock 同理:同一线程连续 lock() 两次,需要 unlock() 两次才能真正释放。
什么是悲观锁、乐观锁?
这是两种并发控制的思想,不是具体的锁类型。
悲观锁
悲观地认为:每次操作都会有并发冲突,所以每次访问数据前都先加锁。
- 代表:synchronized、ReentrantLock
- 适合:写多读少、锁竞争激烈
- 缺点:加锁/解锁开销,线程阻塞
乐观锁
乐观地认为:大多数时候不会冲突,直接操作,提交时再检查是否有冲突。
- 代表:AtomicInteger(CAS)、数据库版本号
- 适合:读多写少、冲突概率低
- 缺点:冲突多时大量重试,ABA 问题
// 乐观锁 CAS 原理(伪代码)
while (true) {
int old = count.get(); // 1. 读取当前值
int newVal = old + 1; // 2. 计算新值
if (count.CAS(old, newVal)) // 3. 提交:若当前值还是 old 就写入
break; // 成功退出,否则重试
// 失败说明有人修改了,重试
}什么是自旋锁?
线程获取锁失败时,不立刻挂起,而是在 CPU 上循环(自旋)等待,期望锁很快会被释放。
优点
避免线程切换(用户态→内核态)的开销。锁持有时间很短时,自旋比挂起更快
缺点
自旋期间占用 CPU。如果锁持有时间长,自旋反而浪费大量 CPU,不如直接阻塞
// 自旋等待的本质(简化)
while (!tryAcquire(lock)) {
// CPU 一直在空转,不阻塞
Thread.onSpinWait(); // JDK 9+ 提示 CPU 自旋暂停
}JVM 的自适应自旋:JDK 6+ 引入自适应自旋,根据上次该锁的自旋成功率动态调整自旋次数。成功率高则多自旋,成功率低则减少自旋次数甚至不自旋,直接膨胀为重量级锁。
适用场景
临界区极短、CPU 核数多的多核场景
不适用
单核 CPU(无法让出 CPU)或持锁时间长的场景
Java 中的体现
轻量级锁升级为重量级锁前,JVM 会先自旋等待
什么是偏向锁、轻量级锁、重量级锁?
这是 JVM 对 synchronized 的三级优化,从低开销到高开销逐步升级,核心信息存储在对象头的 Mark Word 里。
偏向锁
锁偏向第一个获取它的线程。再次获取时只检查线程 ID,无 CAS,无内核调用。开销极小,适合"只有一个线程用"的场景
轻量级锁
有多个线程交替(非同时)竞争锁。用 CAS 在栈帧中创建 Lock Record,不需要操作系统介入。适合"竞争少且短暂"
重量级锁
多线程真正同时竞争。膨胀为操作系统 Mutex,竞争失败的线程被挂起(内核态),开销最大,但保证正确性
三种锁在对象头 Mark Word 中的状态标志不同:

锁升级全过程
偏向锁 → 轻量级锁 → 重量级锁
对象刚创建:初始状态 — 无锁
一个对象刚被 new 出来,Mark Word 处于无锁状态(标志位 001)。
JDK 15 之后默认关闭了偏向锁(因为维护成本高),新创建对象直接进入无锁状态,首次竞争会直接升级为轻量级锁。JDK 6~14 默认开启偏向锁。
Object obj = new Object();
// Mark Word: [hashcode | 0 | 01] 无锁线程A首次加锁:偏向锁 — 线程A独占
线程 A 是第一个获取 synchronized(obj) 的线程。JVM 将线程 A 的 ID 写入 Mark Word,锁偏向线程 A。
之后线程 A 再次进入同步块,只需检查 Mark Word 里的线程 ID 是否是自己,不需要 CAS,不需要 OS 调用,几乎零开销。
// 线程A
synchronized(obj) { ... } // 偏向锁建立
synchronized(obj) { ... } // 再次进入:只检查ID,极快
// Mark Word: [ThreadA_ID | 1 | 01]线程B出现竞争:偏向锁撤销 — 升级为轻量级锁
线程 B 尝试获取同一把锁。JVM 发现锁已偏向线程 A,触发偏向锁撤销(需要等待全局安全点 SafePoint)。
撤销后,若线程 A 已经不在同步块内,锁直接升级为轻量级锁,线程 B 用 CAS 尝试获取。
// 线程B 尝试进入
synchronized(obj) {
// 1. 发现偏向了线程A
// 2. 等待 SafePoint,撤销偏向
// 3. 升级为轻量级锁
// 4. 线程B 在栈帧创建 Lock Record
// 5. CAS 将 Mark Word 指向 Lock Record
}线程交替持锁:轻量级锁 — CAS 无阻塞竞争
轻量级锁下,线程不会被挂起,而是用 CAS 自旋等待锁释放,适合"线程交替持锁、持锁时间短"的场景。
// 轻量级锁加锁(JVM 内部):
// 1. 在当前线程栈帧创建 Lock Record
// 2. 将 obj 的 Mark Word 复制到 Lock Record
// 3. CAS:将 Mark Word 替换为指向 Lock Record 的指针
// 4. 成功→持有;失败→自旋等待
// Mark Word: [Lock Record 指针 | 00]自旋是有上限的(自适应)。若自旋次数过多,说明竞争激烈,继续升级为重量级锁。
自旋失败/多线程同时竞争:重量级锁 — 锁膨胀
自旋超过阈值,或有多个线程同时等待锁,轻量级锁膨胀为重量级锁。
锁膨胀后,Mark Word 指向一个 ObjectMonitor(操作系统 Mutex)。竞争失败的线程被操作系统挂起,让出 CPU,进入 Monitor 的 EntryList 阻塞队列。
// 重量级锁内部(ObjectMonitor)
ObjectMonitor {
_owner → 当前持锁线程
_EntryList → 阻塞等待的线程队列
_WaitSet → 调用 wait() 后挂起的线程
}
// Mark Word: [Monitor指针 | 10]锁释放:重量级锁 — 释放与唤醒
持锁线程执行完同步块,释放锁:
// 释放流程:
// 1. owner 置 null
// 2. 从 EntryList 唤醒一个线程重新竞争
// 3. 被唤醒线程从内核态恢复,重新竞争锁重量级锁的每次加锁/解锁都涉及用户态 ↔ 内核态切换,开销远大于前两种。这就是为什么 JVM 要费力地做偏向锁和轻量级锁优化。
锁只能升级,不能降级
升级路径是单向的(无锁→偏向→轻量级→重量级),JVM 不会自动降级。极少数情况下 JVM 可能在 GC 时降级,但不依赖此行为。
锁升级的本质是:根据竞争激烈程度,用尽量小的开销保证正确性。竞争越激烈,开销越大,同步越安全。