synchronized 和 ReentrantLock 的区别
面试资料大全|各种技术资料-2000G
在 Java 并发编程中,synchronized
和 ReentrantLock
是两种最常用的锁机制,它们在实现线程同步方面各有特点。
一、核心区别全景图
特性 | synchronized | ReentrantLock |
---|---|---|
实现级别 | JVM 原生关键字 | JDK API 类 (java.util.concurrent.locks) |
锁获取方式 | 自动获取与释放 | 手动 lock()/unlock() |
锁类型 | 非公平锁(默认) | 可选择公平锁/非公平锁 |
尝试获取锁 | 不支持 | 支持 tryLock()/tryLock(timeout) |
条件变量 | 单一等待队列 | 支持多个 Condition 队列 |
锁中断 | 不支持中断等待 | 支持 lockInterruptibly() |
性能 | Java 6+ 优化后相当 | 高竞争场景下更优 |
锁绑定 | 与方法/代码块绑定 | 可跨方法使用 |
监控工具 | JVM 内置监控 | 提供 getHoldCount() 等监控方法 |
代码复杂度 | 简单易用 | 需要显式释放锁 |
二、实现原理剖析
1. synchronized 底层机制
public synchronized void method() {// 临界区代码
}// 等价于:
public void method() {this.monitor.enter(); // JVM 指令: monitorentertry {// 临界区代码} finally {this.monitor.exit(); // JVM 指令: monitorexit}
}
锁升级过程:
2. ReentrantLock 实现原理
final ReentrantLock lock = new ReentrantLock();public void method() {lock.lock(); // 基于AQS实现try {// 临界区代码} finally {lock.unlock(); // 必须手动释放}
}
AQS (AbstractQueuedSynchronizer) 核心:
三、关键特性对比详解
1. 公平性控制
synchronized:
- 仅支持非公平锁(线程竞争时不排队)
ReentrantLock:
// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true); // 创建非公平锁(默认)
ReentrantLock unfairLock = new ReentrantLock();
- 公平锁:按等待顺序分配锁
- 非公平锁:允许插队获取锁
2. 锁获取灵活性
synchronized 局限:
- 阻塞式获取,无法中断等待
- 无法设置超时
ReentrantLock 增强:
// 尝试获取锁(立即返回)
if (lock.tryLock()) {try { /* 操作 */ } finally { lock.unlock(); }
}// 带超时尝试
if (lock.tryLock(3, TimeUnit.SECONDS)) {// ...
}// 可中断获取
try {lock.lockInterruptibly();// ...
} catch (InterruptedException e) {// 处理中断
}
3. 条件变量(Condition)
synchronized:
- 仅支持
wait()
/notify()
单一等待队列
ReentrantLock:
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 条件1:非满
Condition notEmpty = lock.newCondition(); // 条件2:非空// 生产者
public void put(Object item) {lock.lock();try {while (isFull()) notFull.await(); // 等待非满条件// ... 添加元素notEmpty.signal(); // 唤醒消费者} finally { lock.unlock(); }
}// 消费者
public Object take() {lock.lock();try {while (isEmpty()) notEmpty.await(); // 等待非空条件// ... 获取元素notFull.signal(); // 唤醒生产者} finally { lock.unlock(); }
}
四、性能对比与优化
1. 历史性能演进
Java 版本 | synchronized 性能 | ReentrantLock 性能 |
---|---|---|
Java 5 | 差(重量级锁) | 显著优于 synchronized |
Java 6 | 引入偏向锁/轻量级锁,大幅提升 | 仍优但差距缩小 |
Java 15+ | 接近 ReentrantLock | 高竞争场景仍保持优势 |
2. 基准测试数据(8线程竞争)
操作 | synchronized (ns/op) | ReentrantLock (ns/op) |
---|---|---|
无竞争获取锁 | 15 | 20 |
中等竞争 | 120 | 95 |
高竞争(16线程) | 450 | 280 |
数据来源:Oracle 官方性能测试 (JMH)
五、使用场景指南
优先使用 synchronized:
// 简单同步场景
public class Counter {private int count;public synchronized void increment() {count++;}
}
- ✅ 简单临界区保护
- ✅ 单方法内的同步
- ✅ 不需要高级特性
优先使用 ReentrantLock:
// 复杂同步控制
public class BankTransfer {private final ReentrantLock lock = new ReentrantLock();public boolean transfer(Account from, Account to, double amount) {// 尝试获取两个账户的锁if (!from.lock.tryLock()) return false;try {if (!to.lock.tryLock()) return false;try {// 转账逻辑return true;} finally {to.lock.unlock();}} finally {from.lock.unlock();}}
}
- ✅ 需要尝试获取锁(tryLock)
- ✅ 需要公平锁机制
- ✅ 需要多个条件变量
- ✅ 需要可中断的锁获取
- ✅ 需要跨方法加锁/解锁
六、最佳实践与陷阱规避
1. ReentrantLock 正确用法
ReentrantLock lock = new ReentrantLock();// 正确:必须用 try-finally 确保释放
lock.lock();
try {// 临界区操作
} finally {lock.unlock(); // 确保在任何情况下都释放锁
}// 错误:忘记解锁(导致死锁)
lock.lock();
// 临界区操作
return; // 如果此处返回,锁永远不会释放!
2. 避免嵌套陷阱
// synchronized 嵌套(正确)
public synchronized void methodA() {methodB(); // 可重入
}public synchronized void methodB() {// ...
}// ReentrantLock 嵌套
public void methodA() {lock.lock();try {methodB(); // 注意重入次数} finally {lock.unlock();}
}public void methodB() {lock.lock(); // 再次获取锁(重入)try { /* ... */ } finally { lock.unlock(); // 必须匹配解锁// 如果此处缺少unlock,锁不会完全释放!}
}
3. 锁性能优化建议
-
减小锁粒度:
// 不推荐:锁整个对象 public synchronized void process() { /* 耗时操作 */ }// 推荐:只锁必要部分 public void process() {// 非同步代码...synchronized(this) {// 临界区} }
-
读写分离:
// 使用 ReentrantReadWriteLock ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); rwLock.readLock().lock(); // 多个读线程可同时进入 rwLock.writeLock().lock(); // 写线程独占
-
锁分段:
// ConcurrentHashMap 的分段锁思想 private final Lock[] locks = new ReentrantLock[16];void update(int key) {int index = key % locks.length;locks[index].lock();try { /* 操作对应分段 */ } finally { locks[index].unlock(); } }
七、现代 Java 的锁发展
1. 虚拟线程(Java 21+)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {executor.submit(() -> {// 在虚拟线程中使用synchronized更安全synchronized(lockObj) {// 不会阻塞操作系统线程}});
}
2. 结构化并发(Java 21+)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {Future<String> user = scope.fork(() -> fetchUser());Future<Integer> order = scope.fork(() -> fetchOrder());scope.join(); // 等待所有任务scope.throwIfFailed(); // 处理异常// 自动管理线程生命周期
}
选型建议
1. 选择 synchronized 当:
- 需要简单的线程同步
- 锁获取释放在同一方法内
- 不需要高级锁特性
- 追求代码简洁性
2. 选择 ReentrantLock 当:
- 需要尝试获取锁(tryLock)
- 需要公平锁机制
- 需要多个条件变量
- 需要可中断的锁获取
- 需要锁的细粒度控制
- 需要跨方法加锁/解锁
3. 通用准则:
- 优先使用 synchronized:Java 6+ 后性能已大幅优化,代码更简洁
- 有明确需求时用 ReentrantLock:当 synchronized 无法满足高级特性时
- 避免过度设计:不要为不存在的性能问题提前优化
面试资料大全|各种技术资料-2000G