synchronized和ReentrantLock的区别
在 Java 并发编程中,synchronized 和 ReentrantLock 都是实现线程同步、保证互斥访问共享资源的关键机制,但它们在设计理念、功能和使用方式上存在显著差异。以下是主要区别:
一. 本质与实现方式
synchronized:- Java 语言内置的关键字。 是 Java 语法的一部分。
- 隐式锁(监视器锁)。 基于 JVM 底层(对象头中的 Mark Word)的
monitorenter和monitorexit指令实现锁的获取和释放。锁的获取和释放由 JVM 自动管理。 - 与对象关联。 锁是附加在对象上的(实例对象或 Class 对象)。
ReentrantLock:java.util.concurrent.locks包中的一个类。 是 JDK 提供的 API。- 显式锁。 开发者需要显式地调用
lock()方法来获取锁,并(必须)在finally块中调用unlock()方法来释放锁。 - 独立的对象。
ReentrantLock本身就是一个锁对象。
二. 锁的获取与释放
synchronized:- 自动获取与释放。 线程进入
synchronized修饰的代码块(方法或代码段)时自动获取关联对象的锁,退出该代码块(正常退出或异常退出)时 JVM 会自动释放锁。开发者无需手动管理。
- 自动获取与释放。 线程进入
ReentrantLock:- 手动获取与释放。 必须显式调用
lock.lock()获取锁。强烈要求在finally块中调用lock.unlock()来确保锁在任何情况下(包括异常)都能被释放,否则可能导致死锁。
- 手动获取与释放。 必须显式调用
三. 功能特性
synchronized:- 基本互斥与重入。 提供基本的互斥性和可重入性(同一线程可多次进入自己持有的锁保护的代码块)。
- 等待/通知机制单一。 通过
Object.wait(),Object.notify(),Object.notifyAll()实现等待/通知,但一个对象的锁只能关联一个等待队列(隐式的条件队列)。
ReentrantLock(功能更丰富):- 基本互斥与重入。 同样提供互斥性和可重入性。
- 尝试非阻塞获取锁:
tryLock()方法尝试获取锁,如果锁当前未被其他线程占用则立即获取成功并返回true,否则立即返回false,线程不会被阻塞。 - 可中断的锁获取:
lockInterruptibly()方法允许在等待锁的过程中响应中断(调用Thread.interrupt())。 - 超时获取锁:
tryLock(long timeout, TimeUnit unit)方法允许在指定的时间内尝试获取锁,超时后返回false。 - 公平性选择: 构造函数可指定锁是公平的 (
new ReentrantLock(true)) 还是非公平的 (new ReentrantLock()或new ReentrantLock(false))。公平锁按线程请求锁的顺序(FIFO)授予锁;非公平锁允许插队,吞吐量通常更高,但可能导致某些线程饥饿。synchronized是非公平的。 - 多个条件队列 (Condition): 可以通过
newCondition()方法创建多个Condition对象。每个Condition相当于一个独立的等待队列。这使得线程可以在不同的条件谓词上等待(例如,生产者等待非满,消费者等待非空),并可以精确地唤醒特定条件上的线程(Condition.signal()/signalAll())。这是synchronized的wait()/notify()机制无法做到的。
四. 性能
- 在 Java 5 及以前版本,
ReentrantLock在高竞争场景下的性能通常优于synchronized。 - 随着 Java 6 对
synchronized进行了大量优化(如偏向锁、轻量级锁、适应性自旋锁、锁消除、锁粗化等),两者的性能差距在大多数常见场景下已经变得很小,甚至synchronized有时更优。 ReentrantLock在超高竞争或者需要利用其高级功能(如tryLock, 公平性)的场景下可能仍有一定优势。- 结论: 性能不应再是选择两者的主要依据。 优先考虑功能需求和代码简洁性。
五. 异常处理
synchronized:- 如果在同步块中抛出异常,JVM 会在退出同步块时自动释放锁。不会因为异常导致锁泄漏。
ReentrantLock:- 如果在调用
lock()之后、unlock()之前的代码中抛出异常,并且unlock()没有被放在finally块中执行,那么锁将永远不会被释放,导致严重死锁。必须使用try-finally确保释放。
- 如果在调用
六. 使用场景
- 优先考虑
synchronized:- 需要实现基本的同步互斥。
- 代码逻辑相对简单。
- 不需要
ReentrantLock提供的高级功能(如超时、可中断、多个条件、公平性)。 - 追求代码简洁性和减少出错可能性(避免忘记
unlock())。
- 考虑使用
ReentrantLock:- 需要尝试非阻塞地获取锁 (
tryLock())。 - 需要可中断地等待锁 (
lockInterruptibly())。 - 需要超时获取锁 (
tryLock(timeout))。 - 需要公平锁机制。
- 需要多个等待条件 (
Condition),实现更复杂的线程协作(如经典的生产者-消费者问题)。 - 在确实存在证据表明
synchronized成为性能瓶颈(且ReentrantLock能解决)的超高竞争场景下。
- 需要尝试非阻塞地获取锁 (
总结对比表
| 特性 | synchronized |
ReentrantLock |
|---|---|---|
| 本质 | Java 语言关键字 | JDK API (一个类) |
| 锁类型 | 隐式锁 (JVM 管理) | 显式锁 (手动管理) |
| 锁获取/释放 | 自动 (进入代码块获取,退出释放) | 手动 (lock() / unlock(),必须在 finally 中释放) |
| 等待/通知 | 单一条件 (wait()/notify()/notifyAll()) |
多个条件 (Condition 对象,await()/signal()/signalAll()) |
| 尝试获取锁 | 不支持 | 支持 (tryLock()) |
| 可中断获取锁 | 不支持 (等待时阻塞不可中断) | 支持 (lockInterruptibly()) |
| 超时获取锁 | 不支持 | 支持 (tryLock(long timeout, TimeUnit unit)) |
| 公平性 | 仅非公平锁 | 可选择公平锁或非公平锁 (构造函数参数) |
| 性能 | Java 6+ 优化后性能良好,一般场景与 ReentrantLock 相当 |
Java 5 优势明显,Java 6+ 在超高竞争或需高级功能时有优势 |
| 异常时释放锁 | 自动释放 | 必须手动在 finally 中释放,否则锁泄漏 |
| 代码简洁性 | 高 (语法简洁) | 低 (需显式加解锁,代码稍繁琐) |
| 适用场景 | 基本同步需求,简单场景 | 需要高级功能 (尝试、中断、超时、公平、多条件) 的场景 |
简单来说:
synchronized: 简单、安全、内置、非公平。适合大多数基础同步需求。ReentrantLock: 灵活、功能强大、显式控制、可选择公平性。当你需要tryLock、可中断锁等待、超时锁、公平锁或多个条件变量 (Condition) 时使用它。但要牢记手动释放锁的责任。
选择哪个取决于具体的需求和偏好。对于大多数标准互斥场景,synchronized 通常是更简洁、更不易出错的选择。当需要更精细的控制或 synchronized 无法满足的高级功能时,ReentrantLock 是强大的替代方案。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 技术之路!
评论


