ReentrantLock 和 synchronized 到底选哪个?源码级对比与选型
面试常考:公平/非公平锁、可中断、超时获取、Condition、锁选型
一、从一道面试题开始
"ReentrantLock 和 synchronized 有什么区别?什么时候用哪个?"------这是 Java 面试中几乎必问的并发问题。大多数人能说出"ReentrantLock 可以公平、可以中断、可以超时",但能说清楚底层实现差异和选型逻辑的不多。本文从源码层面做深度对比。
二、核心特性对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 内置 (Monitor) | Java 层 (AQS) |
| 锁状态 | 对象头 Mark Word | volatile int state |
| 锁释放 | 自动 (出作用域) | 手动 (finally unlock) |
| 可中断 | ❌ | ✅ lockInterruptibly |
| 超时获取 | ❌ | ✅ tryLock(timeout) |
| 公平锁 | ❌ 只有非公平 | ✅ 可选公平/非公平 |
| 条件变量 | 1个 (wait/notify) | 多个 Condition |
| 锁绑定 | 对象 | Lock 对象 |
| 可重入 | ✅ | ✅ |
| 锁消除/粗化 | ✅ JIT优化 | ❌ |
| 内存开销 | 低 (对象头) | 高 (AQS Node + 队列) |
三、加锁流程对比
synchronized 加锁(JVM 层)
javascript
synchronized(obj) {
// code
}
JVM 字节码:
monitorenter ← 进入 Monitor
// code
monitorexit ← 退出 Monitor (正常)
...
monitorexit ← 退出 Monitor (异常)
锁升级链路:
偏向锁 → 轻量级锁 → 重量级锁
(对象头 Mark Word 驱动)
ReentrantLock 加锁(AQS 层)
java
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// code
} finally {
lock.unlock(); // 必须 finally 中释放
}
非公平锁 lock 源码
java
// NonfairSync
final void lock() {
if (compareAndSetState(0, 1)) // 直接 CAS 抢锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 抢不到走 AQS acquire
}
// AQS.acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 再次尝试 CAS / 重入
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// tryAcquire
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // CAS
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 重入
setState(nextc);
return true;
}
return false;
}
公平锁 lock 源码
java
// FairSync
final void lock() {
acquire(1); // 不直接 CAS,先走 acquire
}
final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 关键区别:检查队列中是否有前驱在等待
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
加锁流程对比图
scss
synchronized ReentrantLock (非公平)
┌──────────────────────┐ ┌──────────────────────┐
│ monitorenter │ │ lock.lock() │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ 偏向锁? │ │ CAS抢state │
│ Mark Word │ │ state 0→1 │
└──┬──────┬───┘ └──┬──────┬───┘
成功│ │失败 成功│ │失败
┌──▼┐ ┌──▼────────┐ ┌──▼┐ ┌──▼─────────┐
│进入│ │轻量级锁 │ │进入│ │acquire() │
└───┘ │CAS LockRec│ └───┘ │tryAcquire │
└──┬─────┬──┘ └──┬──────┬───┘
成功│ │失败 成功│ │失败
┌──▼┐ ┌──▼──────┐ ┌──▼┐ ┌──▼────────┐
│进入│ │自旋等待 │ │进入│ │addWaiter │
└───┘ └──┬──────┘ └───┘ │入CLH队列 │
│ └──┬────────┘
┌────▼────┐ ┌──────▼──────┐
│重量级锁 │ │park阻塞 │
│Monitor │ │等unpark唤醒 │
│内核阻塞 │ └─────────────┘
└─────────┘
四、独有特性深度拆解
1. 可中断锁
java
// synchronized 不可响应中断
synchronized (lock) {
// 如果在这里被阻塞,interrupt 无法打断
}
// ReentrantLock 可中断
ReentrantLock lock = new ReentrantLock();
try {
lock.lockInterruptibly(); // 等待锁的过程中可被中断
// 持有锁
} catch (InterruptedException e) {
// 被中断,未获取到锁
} finally {
if (lock.isHeldByCurrentThread())
lock.unlock();
}
源码:
java
public void lockInterruptibly() throws InterruptedException {
acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg); // park 时检查中断,抛异常
}
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 关键:被中断时抛异常而非设置标志
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
2. 超时获取锁
java
// 尝试在 3 秒内获取锁
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// 持有锁
} finally {
lock.unlock();
}
} else {
// 超时未获取到
}
源码核心:
java
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
long lastTime = System.nanoTime();
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
return true;
}
if (nanosTimeout <= 0) { // 超时
cancelAcquire(node);
return false;
}
// 超过 1ms 才 park(否则自旋更高效)
if (nanosTimeout > spinForTimeoutThreshold &&
shouldParkAfterFailedAcquire(p, node))
LockSupport.parkNanos(this, nanosTimeout); // 限时阻塞
long now = System.nanoTime();
if (Thread.interrupted())
throw new InterruptedException();
nanosTimeout -= now - lastTime; // 扣减剩余时间
lastTime = now;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
3. Condition 多条件变量
java
public class TaskQueue {
private final ReentrantLock lock = new ReentrantLock();
private final Condition taskAvailable = lock.newCondition();
private final Condition spaceAvailable = lock.newCondition();
private final Queue<Runnable> queue = new LinkedList<>();
private final int capacity = 10;
public void submit(Runnable task) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity)
spaceAvailable.await(); // 等待空间
queue.offer(task);
taskAvailable.signal(); // 通知消费者
} finally {
lock.unlock();
}
}
public Runnable take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty())
taskAvailable.await(); // 等待任务
Runnable task = queue.poll();
spaceAvailable.signal(); // 通知生产者
return task;
} finally {
lock.unlock();
}
}
}
五、公平锁 vs 非公平锁:性能差异
java
public class FairLockBenchmark {
private static final int THREADS = 10;
private static final int LOOPS = 100000;
static void benchmark(boolean fair) throws Exception {
ReentrantLock lock = new ReentrantLock(fair);
AtomicLong counter = new AtomicLong();
ExecutorService pool = Executors.newFixedThreadPool(THREADS);
long start = System.nanoTime();
for (int i = 0; i < THREADS; i++) {
pool.submit(() -> {
for (int j = 0; j < LOOPS; j++) {
lock.lock();
try {
counter.incrementAndGet();
} finally {
lock.unlock();
}
}
});
}
pool.shutdown();
pool.awaitTermination(60, TimeUnit.SECONDS);
long elapsed = System.nanoTime() - start;
System.out.printf("%s: %dms, count=%d%n",
fair ? "公平锁" : "非公平锁",
elapsed / 1_000_000, counter.get());
}
public static void main(String[] args) throws Exception {
benchmark(false); // 非公平: ~150ms
benchmark(true); // 公平: ~800ms
}
}
为什么非公平锁快?
css
公平锁: 必须排队,每次锁释放 → 线程切换(park/unpark)
T1释放 → 唤醒T2(线程切换) → T2获取 → T2释放 → 唤醒T3...
非公平锁: 新线程直接插队,可能不用阻塞
T1释放 → T3恰好在此时请求 → T3直接CAS获取(无切换)
T1释放 → T3在阻塞 → 唤醒T3(有切换,但不是每次都要)
六、synchronized 的独特优势
1. 锁消除和锁粗化
java
// JIT 逃逸分析 → 锁消除
public String concat(String a, String b) {
StringBuffer sb = new StringBuffer(); // 局部变量不逃逸
sb.append(a); // synchronized 被消除
sb.append(b);
return sb.toString();
}
// 锁粗化
public void batchOps(Object lock) {
synchronized (lock) { op1(); }
synchronized (lock) { op2(); } // 合并为
synchronized (lock) { op3(); } // synchronized(lock){op1();op2();op3();}
}
2. 使用简洁,不会忘记释放
java
// synchronized: 超出作用域自动释放
synchronized (lock) {
doWork();
} // 自动释放
// ReentrantLock: 忘记 unlock → 死锁
lock.lock();
try {
doWork();
} finally {
lock.unlock(); // 必须有
}
3. 内存开销低
yaml
synchronized: 锁信息在对象头 Mark Word (0 额外对象)
ReentrantLock: AQS Node 对象 + ConditionObject + CLH 队列
100万个锁对象:
synchronized: ~16MB (对象头16字节 × 100万)
ReentrantLock: ~40MB (额外对象开销)
七、选型决策
scss
┌──────────────────────┐
│ 需要加锁吗? │
└──────────┬───────────┘
│
┌────────▼────────┐
│ 需要以下特性? │
│ • 公平锁 │
│ • 可中断 │──Yes──▶ ReentrantLock
│ • 超时获取 │
│ • 多个Condition │
└────────┬────────┘
│ No
┌────────▼────────┐
│ 锁持有时间长? │──Yes──▶ ReentrantLock
│ (长任务) │ (避免自旋浪费)
└────────┬────────┘
│ No
┌────────▼────────┐
│ synchronized │
│ (简单场景首选) │
└─────────────────┘
选型建议总结
| 场景 | 推荐 | 原因 |
|---|---|---|
| 简单同步块 | synchronized | 简洁、自动释放、无内存开销 |
| 需要公平排队 | ReentrantLock(fair) | synchronized 不支持公平 |
| 需要中断等待 | ReentrantLock | lockInterruptibly() |
| 需要超时获取 | ReentrantLock | tryLock(timeout) |
| 需要多个条件队列 | ReentrantLock | 多 Condition |
| 锁持有极短 | synchronized | JVM 优化好 |
| 读写分离场景 | ReentrantReadWriteLock | 读读不互斥 |
| 高并发读+少量写 | StampedLock | 乐观读 |
八、面试高频问题速答
Q1: 为什么 ReentrantLock 非公平锁默认开启?
非公平锁吞吐量更高。新来的线程直接 CAS 抢锁,如果恰好锁刚释放,可以直接获取而不用 park/unpark,省去了线程切换开销。极端情况下可能饿死排队线程,但实际竞争场景中 starvation 概率极低。
Q2: ReentrantLock 怎么实现重入?
通过 state 计数。tryAcquire 时如果 state==0 直接 CAS 获取,如果 state>0 且当前线程是持有线程,则 state+1 表示重入。释放时 state-1,减到 0 才真正释放锁。
Q3: synchronized 会被 ReentrantLock 完全替代吗?
不会。synchronized 有 JVM 层面的锁消除、锁粗化优化,内存开销低,使用简洁不会忘记释放。JDK 6+ 锁升级优化后性能也很好。大多场景 synchronized 是够用的,ReentrantLock 只在需要高级特性时使用。
Q4: tryLock() 和 tryLock(0, TimeUnit.NANOSECONDS) 有什么区别?
tryLock() 是无参版本,如果锁空闲就获取,不等待。tryLock(0, NANOSECONDS) 即使传 0 也会走 doAcquireNanos 路程,有入队和取消的开销。但语义不同:无参 tryLock() 不响应中断,有参版本响应中断。
Q5: Condition 的 signal 和 signalAll 区别?
signal() 只唤醒等待队列第一个线程,signalAll() 唤醒所有。signalAll 更安全但效率低,因为被唤醒的线程只有一个能获取锁,其余继续 park。推荐用 signal + while 循环条件检查。
九、总结
ReentrantLock 和 synchronized 不是互斥选择,而是互补关系:
- synchronized:JVM 内置锁,锁升级优化,适合简单同步场景,无内存开销,由 JVM 自动管理释放
- ReentrantLock:基于 AQS 的 Java 层锁,提供公平锁、可中断、超时获取、多 Condition 等高级特性,适合复杂并发控制需求
选型核心逻辑:简单场景用 synchronized,需要高级特性时才用 ReentrantLock。不要为了"看起来更专业"而滥用 ReentrantLock------它带来的内存开销和必须手动释放的风险,在简单场景下是不必要的设计负债。