ReentrantLock 和 synchronized 的区别?

2025年 阅读约 10 分钟 面试指南 · Java面试 · Java并发

深入对比ReentrantLock和synchronized:实现原理(AQS vs monitor)、公平锁/非公平锁、可中断获取、超时获取、Condition多条件等待、性能对比与选型建议,附完整源码分析和面试模拟。

一句话总结

两者都是可重入锁,但实现完全不同:synchronized 是 JVM 层面的隐式锁,基于 monitor 对象,自动释放;ReentrantLock 是 JDK 层面的显式锁,基于 AQS,需手动 unlock。ReentrantLock 额外提供公平锁、可中断获取、超时获取、多条件等待等高级功能,但使用时必须 finally 中释放锁。

初级理解

核心区别一览

性能
特性synchronizedReentrantLock
实现层面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(精细线程协作)。

高级拓展

底层实现对比

维度synchronizedReentrantLock
锁状态存储对象头 Mark WordAQS 的 volatile int state
等待队列ObjectMonitor 的 EntryListAQS 的 CLH 变体队列
阻塞/唤醒操作系统 mutexLockSupport.park/unpark
锁升级偏向→轻量→重量无锁升级,直接 CAS
条件队列ObjectMonitor 的 WaitSetAQS 的 ConditionObject

性能对比

JDK 6 之前,ReentrantLock 性能远优于 synchronized(synchronized 直接使用重量级锁)。JDK 6 之后,synchronized 引入了锁升级机制,两者性能差距很小。在低竞争场景下,synchronized 甚至略优(偏向锁消除 CAS 开销)。

选型建议

场景推荐原因
简单同步synchronized代码简洁,自动释放,不易出错
需要公平锁ReentrantLocksynchronized 不支持公平性
需要可中断ReentrantLocklockInterruptibly()
需要超时ReentrantLocktryLock(time, unit)
多条件协作ReentrantLock多个 Condition
需要监控ReentrantLockgetQueueLength()、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 可以精确唤醒特定条件的等待线程,避免无效唤醒,更适合生产者-消费者等复杂场景。