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 许可协议。转载请注明来源 技术之路!
评论