外观
介绍一下你提到的 synchronized 和 ReentrantLock?该怎么选用?
⭐ 题目日期:
字节 - 2024/12/25
📝 题解:
在 Java 中,synchronized
和 ReentrantLock
是两种实现线程同步的核心工具,但它们在设计理念、功能和使用方式上有显著区别。以下是它们的对比和选型建议:
1. synchronized
(隐式锁)
核心特性
- 语法简单:通过关键字修饰方法或代码块,JVM 自动管理锁的获取和释放。
- 可重入性:线程可以重复获取已持有的锁。
- 非公平锁:默认抢占式获取锁,不保证等待时间长的线程优先获得锁。
- 异常自动释放:线程执行完同步代码或发生异常时,JVM 自动释放锁。
- 优化机制:JDK 6 后引入偏向锁、轻量级锁、重量级锁等优化,性能大幅提升。
使用方式
// 修饰代码块
public void method() {
synchronized (lockObject) {
// 同步代码
}
}
// 修饰实例方法(锁对象是当前实例)
public synchronized void method() { ... }
// 修饰静态方法(锁对象是当前类的 Class 对象)
public static synchronized void staticMethod() { ... }
2. ReentrantLock
(显式锁)
核心特性
- 显式控制:需手动调用
lock()
和unlock()
,通常结合try-finally
确保释放。 - 可重入性:与
synchronized
相同,支持重复获取锁。 - 支持公平锁:构造函数可指定是否为公平锁(默认非公平)。
- 高级功能:
- 可中断锁:
lockInterruptibly()
允许在等待锁时响应中断。 - 超时获取锁:
tryLock(long timeout, TimeUnit unit)
避免无限等待。 - 条件变量:通过
newCondition()
创建多个条件队列,实现精细的线程等待/唤醒(如生产者-消费者模型)。
- 可中断锁:
使用方式
ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 同步代码
} finally {
lock.unlock(); // 必须确保释放锁
}
}
3. 关键区别总结
特性 | synchronized | ReentrantLock |
---|---|---|
锁的获取与释放 | JVM 自动管理 | 需手动调用 lock() 和 unlock() |
公平性 | 仅支持非公平锁 | 支持公平锁和非公平锁 |
功能扩展 | 基础功能 | 支持可中断、超时、条件变量等高级功能 |
性能 | JDK 6+ 优化后接近显式锁 | 高并发竞争时可能更优 |
代码复杂度 | 简单 | 需显式管理,易出错(如忘记释放锁) |
异常处理 | 自动释放锁 | 需在 finally 块中手动释放 |
4. 选型建议
优先选择 synchronized
的场景
- 简单同步需求:如基本的临界区保护,无需高级功能。
- 代码简洁性:避免手动管理锁,减少编码错误。
- 性能敏感但低竞争:在低并发或无激烈锁竞争时,
synchronized
的 JVM 优化可能更高效。
优先选择 ReentrantLock
的场景
- 需要高级功能:
- 可中断锁:避免死锁(如等待锁时响应外部中断)。
- 超时机制:防止线程长时间阻塞。
- 公平锁:减少线程饥饿问题。
- 复杂同步逻辑:
- 使用条件变量(
Condition
)实现多条件等待(如await()
/signal()
)。 - 需要同时持有多个锁(如通过
tryLock
按顺序获取)。
- 使用条件变量(
- 高竞争环境:在极端高并发场景下,
ReentrantLock
的非公平模式可能提供更高吞吐量。
5. 示例对比
场景:可中断的锁等待
// 使用 ReentrantLock 的可中断特性
ReentrantLock lock = new ReentrantLock();
public void performTask() throws InterruptedException {
try {
lock.lockInterruptibly(); // 可中断的锁获取
// 同步代码
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
// 使用 synchronized 无法直接中断等待锁的线程
public synchronized void performTask() {
// 同步代码(无法响应中断)
}
场景:条件变量(生产者-消费者模型)
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产者
public void produce() {
lock.lock();
try {
while (queue.isFull()) {
notFull.await(); // 等待队列非满
}
queue.add(item);
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
6. 注意事项
- 避免死锁:无论是
synchronized
还是ReentrantLock
,需确保锁的获取顺序一致。 - 性能权衡:在大多数场景下,
synchronized
已足够高效;仅在需要高级功能或极端性能优化时使用ReentrantLock
。 - 资源泄漏:使用
ReentrantLock
时务必在finally
块中释放锁。
总结
synchronized
:简洁、安全,适合大多数基础同步场景。ReentrantLock
:灵活、功能强大,适合复杂同步需求或需要精细控制的场景。
根据具体需求选择工具:优先使用 synchronized
,在需要高级功能时转向 ReentrantLock
。