ReentrantReadWriteLock、ReentrantLock、synchronized 全面对比
Java 中实现线程同步的主要方式有三种:synchronized(内置锁)、ReentrantLock(可重入独占锁)、ReentrantReadWriteLock(读写锁)。本文从特性、性能、底层实现、适用场景等维度进行深入对比,帮助开发者在不同场景下做出正确的选型。
一、核心特性对比
| 特性 | synchronized |
ReentrantLock |
ReentrantReadWriteLock |
|---|---|---|---|
| 锁类型 | 独占锁(排他锁) | 独占锁 | 读写锁(读共享、写独占) |
| 可重入性 | ✅ 支持 | ✅ 支持 | ✅ 读锁、写锁均支持可重入 |
| 公平性 | ❌ 非公平(无法改变) | ✅ 可选公平/非公平(默认非公平) | ✅ 可选公平/非公平(默认非公平) |
| 锁降级 | ❌ 不支持 | ❌ 不支持 | ✅ 支持(写锁→读锁) |
| 锁升级 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持(读→写会死锁) |
| 中断响应 | ❌ 不可中断(抛出 InterruptedException 需 wait/join/sleep) | ✅ lockInterruptibly() |
✅ 读锁/写锁均支持 lockInterruptibly() |
| 超时获取 | ❌ 不支持 | ✅ tryLock(long, TimeUnit) |
✅ 读锁/写锁均支持 tryLock(long, TimeUnit) |
| 尝试获取锁 | ❌ 不支持 | ✅ tryLock() |
✅ 读锁/写锁均支持 tryLock() |
| 条件变量 | ✅ wait()/notify()/notifyAll() |
✅ Condition(多个) |
✅ 写锁支持 Condition;读锁不支持 |
| 锁信息查询 | ❌ 无 API | ✅ getHoldCount()、isHeldByCurrentThread() 等 |
✅ 提供读锁/写锁的持有计数、等待线程数等 |
| 释放方式 | 自动(退出同步块) | 手动 unlock()(必须在 finally 中) |
手动 unlock()(必须在 finally 中) |
| 性能(读多写少) | 低(串行) | 低(串行) | 高(读并发) |
| 性能(写多) | 中 | 中 | 中(甚至略低于独占锁) |
二、底层实现与原理
| 锁 | 底层机制 | 原理简述 |
|---|---|---|
synchronized |
JVM 内置监视器锁(Monitor),通过对象头中的 Mark Word 实现 | 依赖 JVM 的偏向锁、轻量级锁、重量级锁升级机制,基于 monitorenter / monitorexit 字节码指令 |
ReentrantLock |
AQS(AbstractQueuedSynchronizer) | 基于 AQS 的独占模式,通过 CAS 修改 state(0/1),失败则进入 CLH 等待队列,支持公平/非公平 |
ReentrantReadWriteLock |
AQS + state 位分割 | 高 16 位存储读锁总重入次数,低 16 位存储写锁重入次数;读锁使用 AQS 共享模式,写锁使用独占模式;读锁重入通过 ThreadLocal 记录 |
三、性能对比与适用场景
3.1 吞吐量对比(8 核 CPU,读耗时 ≈ 写耗时)
| 场景 | synchronized |
ReentrantLock |
ReentrantReadWriteLock |
|---|---|---|---|
| 100% 读 | 低(约 12 ops/us) | 低(约 12 ops/us) | 高(约 90 ops/us) |
| 90% 读 + 10% 写 | 低(约 11 ops/us) | 低(约 11 ops/us) | 中高(约 42 ops/us) |
| 50% 读 + 50% 写 | 低(约 10 ops/us) | 低(约 10 ops/us) | 中(约 15 ops/us) |
| 10% 读 + 90% 写 | 低(约 9 ops/us) | 低(约 9 ops/us) | 低(约 9 ops/us) |
结论:
synchronized和ReentrantLock在纯独占场景下性能几乎相同(现代 JVM 对synchronized做了大量优化)。ReentrantReadWriteLock在读多写少时优势巨大,写多时无优势甚至稍差(CAS 开销)。
3.2 适用场景建议
| 场景 | 推荐锁 | 原因 |
|---|---|---|
| 简单的同步块,代码量少,无需高级功能 | synchronized |
简洁,JVM 自动优化,不易出错 |
| 需要可重入、公平锁、中断、超时、多条件变量 | ReentrantLock |
API 丰富,灵活性高 |
| 读操作远多于写操作(如缓存、配置中心) | ReentrantReadWriteLock |
读并发大幅提升吞吐量 |
| 读极多写极少,且不需要可重入、条件等待 | StampedLock(非本次对比) |
乐观读性能更高 |
四、代码示例对比
4.1 synchronized 示例
java
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() { count++; }
public synchronized int get() { return count; }
}
4.2 ReentrantLock 示例
java
public class ReentrantLockCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try { count++; }
finally { lock.unlock(); }
}
public int get() {
lock.lock();
try { return count; }
finally { lock.unlock(); }
}
}
4.3 ReentrantReadWriteLock 示例
java
public class ReadWriteLockCache {
private final Map<String, Object> cache = new HashMap<>();
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
public Object get(String key) {
rw.readLock().lock();
try { return cache.get(key); }
finally { rw.readLock().unlock(); }
}
public void put(String key, Object value) {
rw.writeLock().lock();
try { cache.put(key, value); }
finally { rw.writeLock().unlock(); }
}
}
五、何时选择哪一种锁?------ 决策树
arduino
开始
│
├─ 是否需要读锁共享(读多写少)?
│ ├─ 是 → 是否需要可重入、条件变量?
│ │ ├─ 是 → ReentrantReadWriteLock
│ │ └─ 否 → StampedLock(性能更高)
│ └─ 否 → 进入独占锁选择
│
├─ 独占锁场景:
│ ├─ 是否需要高级功能(公平、中断、超时、多条件)?
│ │ ├─ 是 → ReentrantLock
│ │ └─ 否 → synchronized(简单可靠)
│
└─ 特殊考量:
├─ 锁降级需求 → 必须用 ReentrantReadWriteLock
├─ 锁升级需求 → 无原生支持,需设计规避
└─ 性能敏感且读多写少 → 优先 ReentrantReadWriteLock 或 StampedLock
六、注意事项与陷阱
| 锁 | 注意事项 |
|---|---|
synchronized |
- 不可中断,等待锁时无法响应中断 - 无法设置超时 - 只有一个条件队列(wait/notify 粒度粗) - 锁信息不可见 |
ReentrantLock |
- 必须手动释放锁 ,务必放在 finally 中 - 公平锁会降低吞吐量 - 锁重入次数无上限(受 int 最大值限制,但通常不会溢出) |
ReentrantReadWriteLock |
- 禁止锁升级 (读→写死锁) - 降级后必须释放读锁 - 读锁持有时间不宜过长(阻塞写锁) - 写锁支持 Condition,读锁不支持 - 重入次数上限 65535 |
七、性能调优建议
- 优先使用
synchronized:除非需要高级功能,否则synchronized足以应对大多数场景,且代码更简洁。 - 使用
ReentrantLock时考虑非公平:除非严格公平要求,否则非公平模式吞吐量更高。 - 读写锁读临界区尽量小:避免长时间持有读锁导致写线程饥饿。
- 考虑锁粒度分解:将一个大锁拆分为多个独立锁(如分段锁),减少竞争。
- 使用
StampedLock替代读写锁:在读极多且不需要重入时,可获得更高性能。
八、总结
| 特性维度 | synchronized |
ReentrantLock |
ReentrantReadWriteLock |
|---|---|---|---|
| 易用性 | ⭐⭐⭐⭐⭐(自动释放) | ⭐⭐⭐(手动释放易错) | ⭐⭐⭐(手动释放,读写分离需理解) |
| 灵活性 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 读并发能力 | ⭐ | ⭐ | ⭐⭐⭐⭐⭐ |
| 写并发能力 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 风险 | 低 | 中(unlock 遗漏) | 高(锁升级、降级释放遗漏) |
最终建议:
- 简单同步 →
synchronized - 需要高级功能(公平、中断、超时、多条件) →
ReentrantLock - 读多写少且需要读并发 →
ReentrantReadWriteLock - 读极多且无需重入 →
StampedLock
根据具体业务场景的读写比例、功能需求和性能目标,选择合适的锁工具是并发编程的关键。