核心原则是:简单场景默认用synchronized,复杂并发场景才用ReentrantLock。
为了帮你快速做出决策,我为你总结了以下选型指南:
🎯 优先使用synchronized的场景
在90% 的常规开发场景中,直接使用synchronized都是最优解。
- 代码简洁安全:它是 JVM 内置的隐式锁,进入同步块自动加锁,执行完或抛出异常自动释放,完全不用担心忘记释放锁导致死锁的问题。
- 常规同步需求:只需要保证简单的互斥性(比如简单的计数器、共享资源读写),不需要任何高级特性。
- 低并发或性能持平:得益于 JDK 1.6 之后的锁升级机制(偏向锁、轻量级锁等),在低并发甚至大多数高并发场景下,它的性能已经和
ReentrantLock相差无几。
🔧 必须使用ReentrantLock的场景
当你需要更细粒度的控制,或者面临复杂的并发协作时,ReentrantLock才是“杀手锏”。如果你有以下任意一个需求,就必须选它:
- 需要尝试获取锁(避免死锁):
使用tryLock()可以尝试非阻塞地获取锁,或者设置超时时间。如果在规定时间内拿不到锁,线程可以放弃等待去执行降级逻辑,从而有效避免死锁。 - 需要可中断的锁:
使用lockInterruptibly(),允许在等待锁的过程中响应中断。如果线程在等锁时被其他线程中断,它可以立刻停止等待并处理异常,而synchronized会一直傻等。 - 需要公平锁:
synchronized只能是非公平锁(允许插队,吞吐量大但可能导致某些线程饥饿)。ReentrantLock可以在构造时传入true开启公平锁,严格按照“先到先得”的顺序获取锁(适合对执行顺序有严格要求的任务调度系统)。 - 需要多条件精准唤醒:
这是ReentrantLock配合Condition的杀手锏。在复杂的生产者-消费者模型中,你可以创建多个Condition(比如notFull和notEmpty),精准唤醒特定条件的线程。而synchronized的wait/notify只能随机唤醒一个或全部唤醒,效率较低。
📊 核心差异速查表
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 底层实现 | JVM 层面(C++实现,基于 Monitor) | JDK 层面(Java API,基于 AQS) |
| 使用方式 | 隐式自动加锁/释放,代码简洁 | 必须手动lock()/unlock()(需在finally中释放) |
| 锁的公平性 | 仅支持非公平锁 | 支持公平锁与非公平锁(默认非公平) |
| 锁获取方式 | 阻塞获取,不可中断,无超时 | 支持超时获取(tryLock)、可中断获取(lockInterruptibly) |
| 条件队列 | 单条件队列(wait/notify) | 支持多条件队列(Condition),可精准唤醒 |
| 性能表现 | JDK 1.6+ 后性能极佳,低竞争下甚至更优 | 高竞争下表现稳定,非公平锁吞吐量高 |
💡 避坑提醒:
如果你决定使用ReentrantLock,千万记得unlock()必须写在finally代码块中!否则一旦临界区代码抛出异常,锁将永远无法释放,直接导致系统死锁。
拓展
ReentrantLock 的“精准唤醒”是通过Condition(条件变量)来实现的。
在传统的synchronized中,一个对象的wait/notify只能对应一个等待队列,唤醒时只能随机唤醒一个或唤醒全部(notifyAll),这就像在广播里喊“谁符合条件谁起来干活”,效率较低且容易误伤。
而ReentrantLock可以绑定多个Condition,就像给不同工种的工人分配了专属的休息室。当需要某个工种的工人干活时,可以直接去对应的休息室精准叫人,互不干扰。
下面我为你演示两个经典的精准唤醒场景:
🎯 场景一:生产者-消费者模型(多条件队列)
在这个模型中,生产者只关心“队列是否已满”,消费者只关心“队列是否为空”。使用两个Condition可以实现精准调度:
importjava.util.LinkedList;importjava.util.Queue;importjava.util.concurrent.locks.Condition;importjava.util.concurrent.locks.ReentrantLock;publicclassProducerConsumerDemo{private