前言
在Java并发编程中,锁机制是保证线程安全的核心手段。synchronized 和 ReentrantLock 是两种最常用的锁实现,面试中经常被要求对比它们的区别。
本文将深入分析两者的底层原理、功能特性、性能差异以及各自的适用场景。
一、快速概览
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 类型 | 关键字(JVM实现) | API类(Java实现) |
| 锁获取/释放 | 自动,JVM保证释放 | 手动,必须在finally中unlock |
| 灵活性 | 较低 | 高,支持tryLock、超时、中断 |
| 公平性 | 非公平锁 | 默认非公平,支持公平锁 |
| 条件变量 | 单一(wait/notify) | 多个Condition |
| 锁升级 | JDK 6后支持 | 无 |
| 底层实现 | 对象头Mark Word + 操作系统Mutex | AQS + CAS |
二、Synchronized 详解
2.1 使用方式
java
// 1. 修饰实例方法:锁当前实例对象
public synchronized void method1() {
// 业务逻辑
}
// 2. 修饰静态方法:锁当前类的Class对象
public static synchronized void method2() {
// 业务逻辑
}
// 3. 修饰代码块:锁指定对象
public void method3() {
synchronized (this) {
// 业务逻辑
}
}
2.2 底层原理
synchronized 的锁信息存储在对象头的 Mark Word 中。
JDK 6 之前的实现
基于操作系统的**互斥量(Mutex)**实现,每次加锁/解锁都需要从用户态切换到内核态,开销较大,被称为"重量级锁"。
JDK 6 之后的锁升级机制
为了减少重量级锁的开销,JVM引入了锁升级机制,锁状态从低到高逐步升级(不可降级):
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
| 锁状态 | 适用场景 | 原理 |
|---|---|---|
| 偏向锁 | 只有一个线程重复获取锁 | 记录线程ID,无需CAS |
| 轻量级锁 | 少量线程竞争 | CAS自旋尝试获取,不自旋过度 |
| 重量级锁 | 多线程激烈竞争 | 阻塞等待,操作系统Mutex |
锁升级的好处:在低竞争场景下,避免了操作系统层面的线程阻塞,大幅提升了性能。
2.3 锁的释放
synchronized 的锁释放是自动的:
- 方法执行完毕自动释放
- 代码块执行完毕自动释放
- 抛出异常时JVM自动释放
优点:不会因忘记释放锁而导致死锁。
三、ReentrantLock 详解
3.1 使用方式
java
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
lock.lock(); // 获取锁
try {
// 业务逻辑
} finally {
lock.unlock(); // 必须手动释放
}
}
}
3.2 核心特性
① 可重入性
同一个线程可以多次获取同一把锁,每获取一次计数器+1,释放一次计数器-1,计数器归零时锁才真正释放。
java
lock.lock();
lock.lock(); // 可重入
try {
// 业务逻辑
} finally {
lock.unlock();
lock.unlock(); // 需要释放两次
}
② 公平锁与非公平锁
java
// 非公平锁(默认)
ReentrantLock nonFairLock = new ReentrantLock();
// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);
公平锁 :线程按照请求顺序获取锁,保证FIFO。
非公平锁:允许插队,吞吐量更高(减少线程挂起/唤醒开销)。
③ 尝试获取锁(tryLock)
java
// 非阻塞获取
if (lock.tryLock()) {
try {
// 获取成功
} finally {
lock.unlock();
}
} else {
// 获取失败,执行其他逻辑
}
// 带超时获取
if (lock.tryLock(1, TimeUnit.SECONDS)) {
// ...
}
④ 可中断获取锁
java
lock.lockInterruptibly(); // 响应中断
⑤ 条件变量(Condition)
java
class BoundedBuffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (isFull()) {
notFull.await(); // 等待不满
}
// 插入元素
notEmpty.signal(); // 唤醒等待非空的线程
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (isEmpty()) {
notEmpty.await(); // 等待非空
}
// 取出元素
notFull.signal(); // 唤醒等待不满的线程
return item;
} finally {
lock.unlock();
}
}
}
优势 :一个 ReentrantLock 可以创建多个 Condition,实现精确唤醒,而 synchronized 的 wait/notify 只能随机唤醒一个或全部。
3.3 底层原理:AQS
ReentrantLock 基于 AQS(AbstractQueuedSynchronizer) 实现。
AQS 维护了一个 volatile int state (同步状态)和一个 FIFO 等待队列:
state = 0:锁未被占用state > 0:锁被占用,且可重入计数
非公平锁的获取流程:
- CAS 尝试将 state 从 0 改为 1,成功则获取锁
- 如果当前线程已持有锁,state + 1(可重入)
- 如果 CAS 失败,进入等待队列排队
四、深度对比
4.1 功能特性对比
| 功能 | synchronized | ReentrantLock |
|---|---|---|
| 可重入 | ✅ | ✅ |
| 支持中断 | ❌ | ✅ (lockInterruptibly) |
| 超时获取 | ❌ | ✅ (tryLock) |
| 公平锁 | ❌ | ✅ |
| 多条件变量 | ❌ | ✅ |
| 尝试非阻塞获取 | ❌ | ✅ |
4.2 性能对比
JDK 6 引入锁升级机制后,synchronized 在低竞争场景下性能大幅提升,甚至优于 ReentrantLock。
- 低竞争场景:两者性能相当,synchronized 略有优势
- 高竞争场景:ReentrantLock 的灵活性(如 tryLock)可以避免不必要的阻塞,表现更好
4.3 使用场景选择
优先使用 synchronized:
- 简单的同步需求
- 方法级别或简单代码块
- 希望代码简洁、不易出错
选择 ReentrantLock:
- 需要尝试获取锁(tryLock)
- 需要超时获取锁
- 需要响应中断
- 需要公平锁
- 需要多条件变量(Condition)
五、常见面试追问
Q1:什么是可重入锁?为什么需要可重入?
答:可重入锁指同一个线程可以多次获取同一把锁,不会造成死锁。这在递归调用、嵌套同步场景中非常必要。
Q2:公平锁和非公平锁的区别?
答:
- 公平锁:按请求顺序获取锁,吞吐量较低,避免饥饿
- 非公平锁:允许插队,吞吐量更高,但可能导致线程饥饿
Q3:synchronized 锁升级的原理?
答:JVM 根据锁的竞争程度,将锁从偏向锁 → 轻量级锁 → 重量级锁逐步升级,减少在低竞争场景下操作系统的上下文切换开销。
六、总结
| 维度 | 结论 |
|---|---|
| 简单场景 | 用 synchronized,代码简洁、安全 |
| 高级功能需求 | 用 ReentrantLock,灵活可控 |
| 性能 | JDK 6 后两者差距不大,根据场景选择 |
💡 面试建议:回答时可以先用表格快速对比,然后结合锁升级和AQS展示深度,最后给出使用建议,体现工程思维。