Synchronized与ReentrantLock深度比较
本文重点对比了synchronized和Re-entrant-Lock。前者是JVM关键字,简单自动;后者是API类,功能强大,支持公平锁、可中断、超时等高级特性,但需手动释放锁。
我们来详细地、系统地比较一下 synchronized 和 ReentrantLock。
synchronized 和 ReentrantLock 都是 Java 中用来实现线程同步的机制,它们的核心目标都是保证在多线程环境下,共享资源能够被安全地访问。尽管目标相同,但它们在实现方式、功能和使用上存在显著的区别。
可以把 synchronized 看作是 Java 内置的、更简单易用的“自动挡”,而 ReentrantLock 则是功能更强大、更灵活的“手动挡”。
核心区别概览
| 特性 | synchronized |
ReentrantLock |
|---|---|---|
| 本质 | Java 关键字,由 JVM 实现 | JUC (java.util.concurrent) 包中的一个类,基于 AQS (AbstractQueuedSynchronizer) 实现 |
| 锁的获取与释放 | 隐式操作,代码块执行完或异常退出后,JVM 自动释放锁 | 显式操作,必须手动调用 lock() 获取和 unlock() 释放锁 |
| 使用便利性 | 简单,不易出错(因为自动释放) | 相对复杂,必须在 finally 块中调用 unlock(),否则可能导致死锁 |
| 可重入性 | 是,隐式支持 | 是,名称已表明,通过内部计数器实现 |
| 公平性 | 非公平锁 | 可选公平/非公平,通过构造函数指定(默认为非公平) |
| 锁等待的可中断性 | 不可中断,线程一旦等待,只能等下去,不能响应中断 | 可中断,通过 lockInterruptibly() 方法 |
| 尝试获取锁 | 不支持,线程会一直阻塞等待 | 支持,通过 tryLock() 和 tryLock(time, unit),可立即返回或超时返回 |
| 条件变量 (Condition) | 单一条件,与锁本身绑定 (wait(), notify(), notifyAll()) |
多个条件,可创建多个 Condition 对象,实现更精细的线程通信 |
| 性能 | Java 1.6 后优化显著(偏向锁、轻量级锁、自旋锁),与 ReentrantLock 性能相当 |
在高竞争下通常有更好的吞吐量,但具体取决于场景和 JDK 版本 |
详细对比解析
1. 本质与层面
synchronized: 是 Java 语言的关键字,其实现是嵌入在 JVM 内部的。JVM 会将其编译成特定的字节码指令(monitorenter和monitorexit),由 JVM 负责锁的获取和释放。ReentrantLock: 是一个标准的 Java 类(java.util.concurrent.locks.ReentrantLock),它提供了面向对象的 API。它的实现依赖于 JUC 框架下的 AQS (AbstractQueuedSynchronizer)。
2. 使用方式与锁的释放
这是最直观的区别。
synchronized使用起来非常简单,你只需要将它放在方法或代码块上。javapublic class SynchronizedCounter { private int count = 0; public synchronized void increment() { count++; } }锁的释放是自动的。当线程执行完同步代码块,或者在执行过程中抛出异常时,JVM 都会确保锁被释放。这大大降低了因忘记释放锁而导致死锁的风险。
ReentrantLock需要手动控制。javaimport java.util.concurrent.locks.ReentrantLock; public class ReentrantLockCounter { private int count = 0; private final ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); // 手动获取锁 try { count++; } finally { lock.unlock(); // 必须在 finally 块中手动释放锁 } } }极其重要:
unlock()调用必须放在finally块中。这是为了保证即使try块中的代码抛出异常,锁也一定会被释放。如果忘记释放锁,其他线程将永远无法获取该锁,造成“死锁”。
3. 功能特性
ReentrantLock 提供了 synchronized 不具备的几个高级功能:
公平锁 (Fair Lock)
synchronized是非公平的。当锁被释放时,任何一个正在等待的线程都有机会获取锁,这可能导致某些线程长时间“饥饿”。ReentrantLock默认也是非公平的,但可以通过构造函数创建公平锁:new ReentrantLock(true)。公平锁会按照线程请求锁的时间顺序(FIFO)来分配锁,但通常会带来额外的性能开销。
可中断的锁等待 (Interruptible Lock)
- 一个正在等待
synchronized锁的线程是不可被中断的,它会一直傻等下去。 ReentrantLock提供了lock.lockInterruptibly()方法。一个正在通过此方法等待锁的线程,如果被其他线程调用了interrupt(),它会停止等待并抛出InterruptedException,从而可以提前结束等待,去做其他事情。这在处理可能发生的死锁时非常有用。
- 一个正在等待
尝试获取锁 (Try Lock)
synchronized没有这个功能。ReentrantLock提供了lock.tryLock()和lock.tryLock(long timeout, TimeUnit unit)。tryLock():尝试立即获取锁,如果成功则返回true,如果锁已被其他线程持有则立即返回false,线程不会阻塞。tryLock(timeout, unit):在指定的时间内尝试获取锁,如果成功则返回true,超时仍未获取则返回false。
这两种方法为避免死锁和实现更灵活的业务逻辑提供了可能。
多条件变量 (Multiple Conditions)
synchronized与Object的wait(),notify(),notifyAll()方法配合使用,但一个锁实例只有一个条件队列。ReentrantLock可以通过lock.newCondition()创建多个Condition对象。每个Condition都有自己的等待队列 (await()) 和通知机制 (signal(),signalAll())。这在实现复杂的生产者-消费者模型时非常有用,例如,可以为“仓库未满”(生产者等待)和“仓库不空”(消费者等待)创建两个不同的Condition,从而可以精确地唤醒特定类型的线程,提高效率。
示例:
javaLock lock = new ReentrantLock(); Condition notFull = lock.newCondition(); // 生产者的条件 Condition notEmpty = lock.newCondition(); // 消费者的条件 // 生产者线程 lock.lock(); try { while (isFull) { notFull.await(); // 等待“未满”这个条件 } // ...生产... notEmpty.signal(); // 通知消费者“不空”了 } finally { lock.unlock(); }
如何选择?
优先选择
synchronized:- 如果锁的竞争不激烈,或者同步逻辑非常简单。
synchronized是 JVM 内置的,经过了大量优化,代码更简洁,不易出错。- “能用
synchronized解决的问题,就不要用ReentrantLock。” 这是大多数情况下的最佳实践。
在以下场景中选择
ReentrantLock:- 需要使用公平锁来保证线程调度的公平性。
- 需要可中断的锁获取操作,以避免线程无限期等待。
- 需要非阻塞地尝试获取锁 (
tryLock),例如,当获取不到锁时可以执行备用逻辑。 - 需要实现复杂的线程通信,使用多个条件变量来分组管理等待的线程。
总结
synchronized 和 ReentrantLock 就像是汽车的自动挡和手动挡。对于日常驾驶(简单的同步需求),自动挡(synchronized)足够好用、方便且安全。但如果你需要上赛道,追求极致的性能和操控(复杂的同步场景),那么手动挡(ReentrantLock)提供的灵活性和高级功能就派上用场了。