一句话总结
两者都是可重入锁,但实现完全不同:synchronized 是 JVM 层面的隐式锁,基于 monitor 对象,自动释放;ReentrantLock 是 JDK 层面的显式锁,基于 AQS,需手动 unlock。ReentrantLock 额外提供公平锁、可中断获取、超时获取、多条件等待等高级功能,但使用时必须 finally 中释放锁。
初级理解
核心区别一览
| 特性 | synchronized | ReentrantLock |
| 实现层面 | JVM 层面,C++ 实现 | JDK 层面,Java 实现(AQS) |
| 锁释放 | 自动释放(代码块结束/异常) | 必须手动 unlock(),通常在 finally |
| 可重入 | ✅ 支持 | ✅ 支持 |
| 公平性 | 非公平 | 可选公平/非公平(构造参数) |
| 可中断 | ❌ 不可中断 | ✅ lockInterruptibly() |
| 超时获取 | ❌ 不支持 | ✅ tryLock(time, unit) |
| 条件等待 | wait/notify(单条件) | Condition(多条件) |
性能| JDK 6+ 优化后接近 | JDK 6+ 优化后接近 |
基本用法对比
// synchronized — 隐式锁,自动释放
public synchronized void syncMethod() {
// 临界区代码
} // 方法结束自动释放锁
synchronized (obj) {
// 临界区代码
} // 代码块结束自动释放锁
// ReentrantLock — 显式锁,必须手动释放
private final ReentrantLock lock = new ReentrantLock();
public void lockMethod() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 必须在 finally 中释放!
}
}
一句话总结:synchronized 简单安全(自动释放),ReentrantLock 功能更强(公平锁、可中断、超时、多条件),但需要手动管理。
中级深入
公平锁 vs 非公平锁
ReentrantLock 通过构造参数选择公平性:
// 非公平锁(默认,性能更好)
ReentrantLock lock = new ReentrantLock();
ReentrantLock lock = new ReentrantLock(false);
// 公平锁(按等待顺序获取,避免饥饿)
ReentrantLock fairLock = new ReentrantLock(true);
公平锁的代价:每次获取锁都要检查队列,线程切换更频繁,吞吐量低于非公平锁。synchronized 只有非公平模式。
可中断获取 — lockInterruptibly()
synchronized 等待锁时无法被中断,线程会一直阻塞。ReentrantLock 的 lockInterruptibly() 允许在等待时响应中断:
// synchronized — 无法中断等待
synchronized (obj) {
// 如果锁被占用,线程一直阻塞,interrupt() 无效
}
// ReentrantLock — 可中断等待
try {
lock.lockInterruptibly(); // 等待时可被中断
try {
// 临界区
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 被中断后的处理
}
超时获取 — tryLock()
// tryLock() — 立即返回,不等待
if (lock.tryLock()) {
try {
// 获取到锁
} finally {
lock.unlock();
}
} else {
// 没获取到锁,做其他事情
}
// tryLock(time, unit) — 等待指定时间
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 1秒内获取到锁
} finally {
lock.unlock();
}
} else {
// 超时未获取到
}
} catch (InterruptedException e) {
// 等待中被中断
}
Condition 多条件等待
synchronized 只能通过 wait/notify 在一个条件上等待。ReentrantLock 可以创建多个 Condition,实现更精细的线程协作:
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
// 生产者
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 队列满,等待 notFull 条件
}
queue.add(item);
notEmpty.signal(); // 唤醒等待 notEmpty 的消费者
} finally {
lock.unlock();
}
}
// 消费者
public Object take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 队列空,等待 notEmpty 条件
}
Object item = queue.remove();
notFull.signal(); // 唤醒等待 notFull 的生产者
return item;
} finally {
lock.unlock();
}
}
中级要点:ReentrantLock 的三大高级功能:公平锁(避免饥饿)、可中断/超时获取(避免死锁)、多 Condition(精细线程协作)。
高级拓展
底层实现对比
| 维度 | synchronized | ReentrantLock |
| 锁状态存储 | 对象头 Mark Word | AQS 的 volatile int state |
| 等待队列 | ObjectMonitor 的 EntryList | AQS 的 CLH 变体队列 |
| 阻塞/唤醒 | 操作系统 mutex | LockSupport.park/unpark |
| 锁升级 | 偏向→轻量→重量 | 无锁升级,直接 CAS |
| 条件队列 | ObjectMonitor 的 WaitSet | AQS 的 ConditionObject |
性能对比
JDK 6 之前,ReentrantLock 性能远优于 synchronized(synchronized 直接使用重量级锁)。JDK 6 之后,synchronized 引入了锁升级机制,两者性能差距很小。在低竞争场景下,synchronized 甚至略优(偏向锁消除 CAS 开销)。
选型建议
| 场景 | 推荐 | 原因 |
| 简单同步 | synchronized | 代码简洁,自动释放,不易出错 |
| 需要公平锁 | ReentrantLock | synchronized 不支持公平性 |
| 需要可中断 | ReentrantLock | lockInterruptibly() |
| 需要超时 | ReentrantLock | tryLock(time, unit) |
| 多条件协作 | ReentrantLock | 多个 Condition |
| 需要监控 | ReentrantLock | getQueueLength()、hasQueuedThreads() 等 |
高级加分项:能说出 JDK 6 前后性能差异的原因(锁升级),能根据场景给出选型建议,能解释 Condition 比 wait/notify 的优势(精确唤醒、多条件)。
实战场景
场景一:避免死锁 — 用 tryLock 超时
public boolean transfer(Account from, Account to, int amount) {
while (true) {
if (from.lock.tryLock()) {
try {
if (to.lock.tryLock()) {
try {
from.debit(amount);
to.credit(amount);
return true;
} finally {
to.lock.unlock();
}
}
} finally {
from.lock.unlock();
}
}
// 没获取到全部锁,随机等待后重试
Thread.sleep(random.nextInt(100));
}
}
场景二:读写锁 — ReentrantReadWriteLock
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
public Object get(String key) {
readLock.lock();
try {
return cache.get(key); // 多个线程可同时读
} finally {
readLock.unlock();
}
}
public void put(String key, Object value) {
writeLock.lock();
try {
cache.put(key, value); // 写时独占
} finally {
writeLock.unlock();
}
}
常见坑
| 坑 | 原因 | 解决方案 |
| 忘记 unlock | 异常导致锁永不释放 | lock 后紧跟 try,finally 中 unlock |
| lock 在 try 外 | lock 抛异常时 finally 中 unlock 报错 | lock() 放在 try 之前 |
| Condition 用错锁 | Condition 必须从对应的 Lock 创建 | lock.newCondition() |
面试模拟
Q:ReentrantLock 和 synchronized 的区别?
A:synchronized 是 JVM 层面的隐式锁,自动释放;ReentrantLock 是 JDK 层面的显式锁,需手动 unlock。ReentrantLock 额外提供公平锁、可中断获取、超时获取、多 Condition等功能。JDK 6 后两者性能接近,简单场景优先 synchronized,需要高级功能用 ReentrantLock。
Q:为什么 ReentrantLock 的 lock() 要放在 try 外面?
A:如果 lock() 放在 try 里面,当 lock() 抛出异常时,finally 中的 unlock() 仍会执行,但此时锁并未被当前线程持有,会抛出 IllegalMonitorStateException,掩盖了原始异常。正确做法是 lock() 在 try 之前,确保只有成功获取锁后才进入 try-finally。
Q:Condition 和 wait/notify 的区别?
A:wait/notify 是 Object 的方法,配合 synchronized 使用,只有一个等待队列,notify 随机唤醒一个。Condition 配合 Lock 使用,可以创建多个条件队列,signal 可以精确唤醒特定条件的等待线程,避免无效唤醒,更适合生产者-消费者等复杂场景。