一句话结论
-
synchronized:语法级、JVM 内建监视器锁,简单、自动释放,功能少但足够稳。
-
ReentrantLock:基于 AQS 的可重入显式锁,可中断/可超时/可选公平/多条件队列,功能强更灵活,但用法更复杂、需手动解锁。
共同点
-
都是可重入的互斥锁。
-
都提供happens-before 语义:解锁/退出同步块前的写,对随后同一锁上的加锁/进入同步块可见。
-
都可用于保护临界区,避免数据竞争。
主要差异(速览表)
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现 | JVM 监视器(enter/exit monitor) | AQS(队列 + CAS 计数) |
| 获取/释放 | 进入/退出代码块自动完成 | lock() / unlock()(需 try/finally) |
| 可中断获取 | 不支持 | lockInterruptibly() 支持 |
| 超时/尝试获取 | 不支持 | tryLock() / tryLock(timeout) |
| 公平性 | 不支持 | 构造可选公平/非公平 |
| 条件队列 | wait/notify/notifyAll(一个等待集) | newCondition() 可建多个条件队列 |
| 调试与状态 | 无法查询持有者/重入次数 | isHeldByCurrentThread() / getHoldCount() 等 |
| 性能取向 | 低/中竞争下非常优化,语法最简 | 高竞争/复杂条件下更可控(但要小心误用) |
| 易用性/出错面 | 简单、自动释放,不易漏锁 | 灵活但易漏 unlock()、易用错条件队列 |
代码对照
synchronized(最简)
private final Object lock = new Object();
void put(X x) {
synchronized (lock) {
// 临界区
} // 自动释放
}
条件等待(只有一个等待集):
synchronized (lock) {
while (!ready) lock.wait(); // 可能虚假唤醒,必须 while 检查
// ...
}
// 另处:
synchronized (lock) {
ready = true;
lock.notifyAll();
}
ReentrantLock(更灵活)
import java.util.concurrent.locks.*;
private final ReentrantLock lock = new ReentrantLock(); // 可 new ReentrantLock(true) 公平
void put(X x) {
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须在 finally 释放
}
}
可中断/超时/多条件:
if (lock.tryLock(50, TimeUnit.MILLISECONDS)) { // 超时获取
try { /* ... */ } finally { lock.unlock(); }
}
lock.lockInterruptibly(); // 可响应中断
Condition notEmpty = lock.newCondition();
lock.lock();
try {
while (!hasData) notEmpty.await(); // await/signal 多队列可分离条件
// ...
} finally {
lock.unlock();
}
选型建议
-
首选 synchronized:简单同步、低~中等竞争、无需可中断/超时/公平/多条件,大多数业务代码够用且可读性好。
-
选 ReentrantLock,当你需要:
- 可中断(避免线程无法被取消)或超时获取(防止长时间阻塞)。
- 公平策略(减轻饥饿)。
- 多个条件队列(不同条件分流,避免“广播唤醒”)。
- 更丰富的状态查询/诊断(定位死锁/卡顿)。
-
Android 提醒:两者都是阻塞式。不要在主线程长时间 lock();如要在协程中用,请放到 Dispatchers.IO,或优先用挂起友好的 Mutex。
常见坑
- ReentrantLock 漏解锁:必须 try/finally 包裹。
- 条件等待误用:await()/wait() 必须放在持锁区,且用 while 重新校验条件。
- 滥用公平锁:公平性降低吞吐,仅在确实有饥饿时启用。
- 把锁当读写锁用:需要读多写少并发,请考虑 ReentrantReadWriteLock 或 StampedLock(后者不可重入、适合乐观读)。