深入对比:Java 可重入锁(ReentrantLock)与 synchronized 的异同
一、前言
在 Java 并发编程中,synchronized 和 ReentrantLock 是最常用的两种锁机制。synchronized 是 JVM 层面的关键字,而 ReentrantLock 是 JDK 提供的 API 层面的可重入锁。本文将从多个维度深入对比二者的异同,帮助读者在实际开发中做出合适的选择。
二、相同点
1. 可重入性(Reentrancy)
两者都支持可重入:同一个线程在持有锁的情况下,可以再次获取同一把锁而不被阻塞。
synchronized 可重入示例:
public class SynchronizedReentrantDemo {
public synchronized void outer() {
System.out.println("进入 outer 方法");
inner(); // 同一个线程再次获取锁
}
public synchronized void inner() {
System.out.println("进入 inner 方法");
}
public static void main(String[] args) {
new SynchronizedReentrantDemo().outer();
}
}
ReentrantLock 可重入示例:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockReentrantDemo {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
System.out.println("进入 outer 方法");
inner(); // 同一个线程再次获取锁
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock();
try {
System.out.println("进入 inner 方法");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new ReentrantLockReentrantDemo().outer();
}
}
2. 互斥性(Mutual Exclusion)
两者都保证互斥访问:同一时刻最多只有一个线程可以持有锁,其他尝试获取锁的线程会被阻塞或等待。
三、不同点
1. 锁的获取与释放方式
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 获取锁 | 隐式获取,由 JVM 自动管理 | 显式调用 lock() 或 tryLock() |
| 释放锁 | 隐式释放,退出同步块/方法时自动释放 | 必须显式调用 unlock(),通常放在 finally 块中 |
ReentrantLock 必须手动释放锁:
// ✅ 正确用法
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 确保释放锁
}
// ❌ 错误用法 —— 忘记释放锁可能导致死锁
lock.lock();
// 临界区代码
// 忘记调用 lock.unlock() —— 其他线程将永远无法获取锁
2. 公平性(Fairness)
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 公平策略 | 不支持公平锁,是非公平的 | 支持公平锁和非公平锁(默认非公平) |
ReentrantLock 公平锁示例:
// 公平锁:按照线程请求锁的先后顺序分配锁
ReentrantLock fairLock = new ReentrantLock(true);
// 非公平锁(默认):允许插队,可能导致线程饥饿
ReentrantLock unfairLock = new ReentrantLock(false);
ReentrantLock defaultLock = new ReentrantLock(); // 等价于 false
注意:公平锁会牺牲一定的吞吐量来保证公平性。在大多数场景下,非公平锁的性能优于公平锁。
3. Condition 条件变量
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 等待/通知机制 | 只有一个隐式的条件队列(wait() / notify() / notifyAll()) |
支持多个 Condition 对象,可以精细控制线程的等待和唤醒 |
synchronized 的 wait/notify:
public class SynchronizedConditionDemo {
private final Object lock = new Object();
private boolean ready = false;
public void await() throws InterruptedException {
synchronized (lock) {
while (!ready) {
lock.wait(); // 等待
}
System.out.println("条件满足,继续执行");
}
}
public void signal() {
synchronized (lock) {
ready = true;
lock.notifyAll(); // 唤醒所有等待线程
}
}
}
ReentrantLock 的 Condition(支持多个条件队列):
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 队列未满条件
private final Condition notEmpty = lock.newCondition(); // 队列非空条件
private final Object[] buffer = new Object[10];
private int putIndex, takeIndex, count;
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (count == buffer.length) {
notFull.await(); // 队列已满,等待
}
buffer[putIndex] = item;
if (++putIndex == buffer.length) putIndex = 0;
count++;
notEmpty.signal(); // 唤醒等待的消费者
} finally {
lock.unlock();
}
}
@SuppressWarnings("unchecked")
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 队列为空,等待
}
Object item = buffer[takeIndex];
if (++takeIndex == buffer.length) takeIndex = 0;
count--;
notFull.signal(); // 唤醒等待的生产者
return item;
} finally {
lock.unlock();
}
}
}
Condition 的优势在于:一个锁可以绑定多个条件,避免使用
notifyAll()盲目唤醒所有等待线程,提高了效率。
4. 可中断性(Interruptibility)
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 响应中断 | 获取锁时不可中断,线程会一直阻塞直到获取锁 | 支持中断响应,可以通过 lockInterruptibly() 在等待锁时响应中断 |
ReentrantLock 可中断获取锁:
import java.util.concurrent.locks.ReentrantLock;
public class InterruptibleLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
try {
// 可中断地获取锁:如果线程被中断,会抛出 InterruptedException
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 获取到锁");
Thread.sleep(5000); // 模拟耗时操作
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断,放弃等待锁");
}
}
public static void main(String[] args) throws InterruptedException {
InterruptibleLockDemo demo = new InterruptibleLockDemo();
Thread t1 = new Thread(demo::performTask, "线程1");
Thread t2 = new Thread(demo::performTask, "线程2");
t1.start();
Thread.sleep(100); // 确保 t1 先拿到锁
t2.start();
Thread.sleep(100);
t2.interrupt(); // 中断 t2,t2 将放弃等待锁
}
}
synchronized 在等待锁时无法被中断,这可能导致死锁后无法恢复。ReentrantLock 的
lockInterruptibly()提供了一种"可取消"的锁获取机制。
5. 尝试获取锁(tryLock)
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 尝试获取 | 不支持 | 支持 tryLock() 和 tryLock(long timeout, TimeUnit unit),可设置超时 |
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void tryAcquire() {
// 立即尝试,获取不到就返回 false
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " 立即获取到锁");
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 未能获取到锁,做其他事情");
}
}
public void tryAcquireWithTimeout() {
try {
// 等待 1 秒,如果 1 秒内获取不到则放弃
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 在超时内获取到锁");
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 超时未获得到锁");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断");
}
}
}
6. 锁的实现与性能
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层级 | JVM 层面(基于 monitor 对象) | JDK 层面(基于 AbstractQueuedSynchronizer, AQS) |
| JDK 5 性能 | 较差(重量级锁) | 较好 |
| JDK 6+ 性能 | 显著优化(锁升级:偏向锁 → 轻量级锁 → 重量级锁) | 与 synchronized 差距缩小 |
JDK 6 之后,synchronized 引入了锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),在无竞争或低竞争场景下性能大幅提升。在大多数现代 JDK 版本中,两者性能相差不大,synchronized 甚至在某些场景下更优。
7. 其他特性对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 底层实现 | 基于 monitor 对象,借助操作系统 mutex | 基于 AQS(AbstractQueuedSynchronizer) |
| 锁类型 | 非公平锁 | 公平锁 / 非公平锁 |
| 锁状态查询 | 不支持 | 支持 isLocked()、getQueueLength()、hasQueuedThreads() |
| 代码写法 | 简洁,关键字修饰 | 需手动加锁/解锁,代码稍复杂 |
| 异常处理 | 锁自动释放,不易出现死锁 | 忘记 unlock() 可能导致死锁 |
| 调试友好度 | 栈信息清晰,JVM 有优化 | 栈信息稍复杂 |
四、完整对比总结表
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 可重入性 | ✅ 支持 | ✅ 支持 |
| 互斥性 | ✅ 保证 | ✅ 保证 |
| 加锁/解锁方式 | 隐式(JVM 自动管理) | 显式(需在 finally 中手动释放) |
| 公平性 | ❌ 仅非公平 | ✅ 公平 + 非公平(可选) |
| Condition | ❌ 仅一个隐式条件队列 | ✅ 支持多个 Condition 对象 |
| 可中断 | ❌ 不可中断 | ✅ 支持 lockInterruptibly() |
| tryLock | ❌ 不支持 | ✅ 支持超时尝试 |
| 性能 (JDK 5) | 较差 | 较好 |
| 性能 (JDK 8+) | 优秀(经锁升级优化) | 优秀,与 synchronized 接近 |
| 锁状态查询 | ❌ 不支持 | ✅ 支持 |
| 代码简洁性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 灵活度 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
五、如何选择?
优先使用 synchronized 的场景
- 代码简洁优先 —— 不需要手动释放锁,不易出错
- 低竞争场景 —— JDK 6+ 经锁升级优化后很高效
- 不需要额外特性 —— 不需要公平锁、Condition、可中断等高级功能
- 团队规范 —— 大多数团队默认优先使用 synchronized
优先使用 ReentrantLock 的场景
- 需要公平锁 —— 如某些对公平性有要求的调度场景
- 需要多个 Condition —— 如生产者-消费者模式(多条件等待/唤醒)
- 需要可中断的锁获取 —— 避免死锁后无法恢复
- 需要尝试获取锁(超时) —— 避免无限期等待
- 需要查询锁状态 —— 调试或监控需求
六、经典面试题
Q1:synchronized 和 ReentrantLock 的性能谁更好?
答:在 JDK 6 之前,synchronized 性能较差,ReentrantLock 有明显优势。JDK 6 引入了锁升级机制后,synchronized 在无竞争时近乎无开销,两者性能差距大幅缩小。在 JDK 8+ 中,两者性能基本相当,synchronized 在某些场景下甚至更优。选择时不应以性能为主要考量,而应根据功能需求决定。
Q2:ReentrantLock 如何实现公平锁?
答:ReentrantLock 基于 AQS(AbstractQueuedSynchronizer)实现。公平锁模式下,线程获取锁时会先检查同步队列中是否有等待时间更长的线程;如果有,则当前线程加入队列尾部,不尝试抢占。非公平锁模式下,线程会先尝试 CAS 抢锁,抢不到再入队。
Q3:synchronized 底层是如何实现的?
答:synchronized 基于 Monitor 对象实现,编译后会在同步代码块前后生成
monitorenter和monitorexit字节码指令。JDK 6 后引入了锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),通过对象头中的 Mark Word 标记锁状态,尽量减少操作系统级别的线程阻塞。
七、总结
| 结论 | 说明 |
|---|---|
| 功能相似 | 两者都提供可重入的互斥锁,保证线程安全 |
| synchronized 更简洁 | 隐式加解锁,代码更少,不易出错 |
| ReentrantLock 更灵活 | 支持公平锁、Condition、可中断、tryLock 等高级特性 |
| 性能差距已缩小 | JDK 6+ 两者性能接近,synchronized 在某些场景反而更优 |
| 推荐原则 | 能用 synchronized 就用 synchronized;需要高级特性时用 ReentrantLock |
最后的建议:在日常开发中,如果 synchronized 能满足需求,优先使用 synchronized;只有当你确实需要公平锁、多个 Condition、可中断获取、超时尝试等高级功能时,再考虑使用 ReentrantLock。