1. ReentrantLock显式锁深度解析
在多线程编程中,锁机制是保证线程安全的核心工具。Java提供了两种锁机制:synchronized隐式锁和ReentrantLock显式锁。今天我们就来深入探讨这个比synchronized更灵活、功能更强大的锁工具。
ReentrantLock是Java并发包(java.util.concurrent.locks)中的一个重要类,它实现了Lock接口,提供了比synchronized更丰富的锁操作功能。与synchronized不同,ReentrantLock需要显式地获取和释放锁,这给了开发者更大的控制权,但也带来了更多的责任。
1.1 ReentrantLock的核心特性
ReentrantLock之所以被称为"可重入锁",是因为它允许同一个线程多次获取同一把锁。这种特性在递归调用场景中特别有用。除此之外,它还具备以下重要特性:
- 公平性选择:可以创建公平锁或非公平锁
- 可中断的锁获取:支持lockInterruptibly()方法
- 超时获取锁:tryLock()方法可以设置超时时间
- 条件变量支持:可以创建多个Condition对象
1.2 基本使用模式
使用ReentrantLock的标准模式如下:
java复制ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 确保锁被释放
}
}
这种try-finally模式非常重要,它能确保即使临界区代码抛出异常,锁也能被正确释放,避免死锁情况。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ReentrantLock与synchronized的对比
2.1 功能对比
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 可重入性 | 支持 | 支持 |
| 公平锁 | 支持 | 不支持 |
| 可中断 | 支持 | 不支持 |
| 超时获取 | 支持 | 不支持 |
| 条件变量 | 支持 | 有限支持 |
| 锁释放 | 显式 | 自动 |
2.2 性能考量
在低竞争情况下,synchronized的性能通常优于ReentrantLock,因为synchronized是JVM内置特性,可以进行更多优化。但在高竞争情况下,ReentrantLock通常表现更好,特别是使用公平锁策略时。
注意:不要仅仅因为性能原因就选择ReentrantLock。只有在需要synchronized无法提供的特性时,才应该考虑使用ReentrantLock。
3. 高级特性详解
3.1 公平锁与非公平锁
ReentrantLock的构造函数可以接受一个boolean参数,指定是否创建公平锁:
java复制ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(); // 非公平锁(默认)
公平锁会按照线程请求锁的顺序来分配锁,而非公平锁则允许"插队",这可能导致某些线程长时间获取不到锁(饥饿),但通常能提供更高的吞吐量。
3.2 可中断的锁获取
ReentrantLock提供了lockInterruptibly()方法,允许在等待锁的过程中响应中断:
java复制try {
lock.lockInterruptibly();
// 临界区代码
} catch (InterruptedException e) {
// 处理中断
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
这个特性在需要实现可取消任务时非常有用。
3.3 尝试获取锁与超时
tryLock()方法可以尝试获取锁而不阻塞,或者设置超时时间:
java复制if (lock.tryLock()) {
try {
// 获取锁成功
} finally {
lock.unlock();
}
} else {
// 获取锁失败
}
// 带超时的尝试
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 获取锁成功
} finally {
lock.unlock();
}
} else {
// 超时未获取锁
}
3.4 条件变量(Condition)
ReentrantLock可以创建多个Condition对象,这比Object的wait/notify机制更灵活:
java复制ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待条件
lock.lock();
try {
while (!conditionMet) {
condition.await();
}
// 条件满足后的处理
} finally {
lock.unlock();
}
// 通知条件
lock.lock();
try {
conditionMet = true;
condition.signalAll();
} finally {
lock.unlock();
}
4. 实战应用与最佳实践
4.1 典型应用场景
- 缓存实现:在实现线程安全的缓存时,ReentrantLock可以提供更细粒度的控制
- 资源池管理:如数据库连接池,需要复杂的等待/通知机制
- 高性能计数器:需要精确控制并发访问的统计场景
- 复杂事务:需要可中断、超时等高级特性的场景
4.2 最佳实践
- 总是使用try-finally释放锁:确保锁在任何情况下都能被释放
- 避免锁嵌套:虽然ReentrantLock支持重入,但过度嵌套会使代码难以维护
- 合理选择公平性:默认使用非公平锁,只有在确实需要公平性时才使用公平锁
- 控制锁粒度:锁的粒度应该尽可能小,持有时间尽可能短
- 考虑使用ReadWriteLock:对于读多写少的场景,ReentrantReadWriteLock可能更合适
4.3 常见问题与解决方案
问题1:忘记释放锁
这是使用ReentrantLock最常见的错误。解决方案是严格遵循try-finally模式:
java复制lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
问题2:锁泄露
当临界区代码抛出异常时,如果没有正确处理,可能导致锁泄露。确保在finally块中释放锁。
问题3:死锁
多个锁以不同顺序获取可能导致死锁。解决方案:
- 总是以固定顺序获取多个锁
- 使用tryLock()和超时机制
- 使用锁层次结构
问题4:性能问题
过度使用锁会导致性能下降。考虑:
- 减小锁粒度
- 使用无锁数据结构(如ConcurrentHashMap)
- 使用乐观锁
5. 性能调优与监控
5.1 锁竞争监控
ReentrantLock提供了一些有用的监控方法:
java复制ReentrantLock lock = new ReentrantLock();
// 获取等待锁的线程数
int queuedThreads = lock.getQueueLength();
// 查询是否有线程在等待锁
boolean hasQueuedThreads = lock.hasQueuedThreads();
// 查询当前线程是否持有锁
boolean heldByCurrentThread = lock.isHeldByCurrentThread();
// 查询锁是否被任何线程持有
boolean isLocked = lock.isLocked();
这些方法可以用于监控和调试锁竞争情况。
5.2 锁争用优化策略
- 减小锁粒度:将一个大锁拆分为多个小锁
- 锁分段:如ConcurrentHashMap的实现方式
- 读写分离:使用ReentrantReadWriteLock
- 无锁算法:考虑使用原子变量(AtomicInteger等)
- 减少锁持有时间:只在对共享数据操作时持有锁
5.3 基准测试示例
下面是一个简单的基准测试,比较synchronized和ReentrantLock的性能:
java复制public class LockBenchmark {
private static final int THREADS = 16;
private static final int ITERATIONS = 1_000_000;
private int counterSync = 0;
private int counterLock = 0;
private final Object syncLock = new Object();
private final ReentrantLock reentrantLock = new ReentrantLock();
public void testSynchronized() throws InterruptedException {
counterSync = 0;
ExecutorService executor = Executors.newFixedThreadPool(THREADS);
long start = System.currentTimeMillis();
for (int i = 0; i < THREADS; i++) {
executor.submit(() -> {
for (int j = 0; j < ITERATIONS; j++) {
synchronized (syncLock) {
counterSync++;
}
}
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.HOURS);
long duration = System.currentTimeMillis() - start;
System.out.printf("Synchronized: %d ms%n", duration);
}
public void testReentrantLock() throws InterruptedException {
counterLock = 0;
ExecutorService executor = Executors.newFixedThreadPool(THREADS);
long start = System.currentTimeMillis();
for (int i = 0; i < THREADS; i++) {
executor.submit(() -> {
for (int j = 0; j < ITERATIONS; j++) {
reentrantLock.lock();
try {
counterLock++;
} finally {
reentrantLock.unlock();
}
}
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.HOURS);
long duration = System.currentTimeMillis() - start;
System.out.printf("ReentrantLock: %d ms%n", duration);
}
}
在实际测试中,结果会因JVM版本、硬件配置和线程竞争程度而有所不同。一般来说:
- 低竞争时,synchronized可能更快
- 高竞争时,ReentrantLock通常表现更好
- 公平锁通常比非公平锁慢
6. 源码解析与实现原理
6.1 内部实现概述
ReentrantLock的内部实现依赖于AbstractQueuedSynchronizer(AQS)框架。AQS是Java并发包中实现同步器的基础框架,它使用一个FIFO队列来管理获取锁的线程。
ReentrantLock有公平和非公平两种实现,分别对应FairSync和NonfairSync两个内部类。它们都继承自Sync,而Sync又继承自AQS。
6.2 关键方法解析
lock()方法实现(非公平锁)
java复制final void lock() {
if (compareAndSetState(0, 1)) // 尝试直接获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 加入队列等待
}
非公平锁会先尝试直接获取锁(CAS操作),如果失败才进入队列等待。
tryLock()方法实现
java复制public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
tryLock()实际上就是非公平的获取尝试,它不会将线程加入等待队列。
6.3 状态管理
ReentrantLock使用AQS的state字段来记录锁的状态:
- state == 0:锁未被持有
- state > 0:锁被持有,数值表示重入次数
- 持有锁的线程记录在exclusiveOwnerThread字段中
这种设计使得ReentrantLock能够实现可重入特性。
7. 扩展应用与进阶话题
7.1 与ReadWriteLock结合使用
在某些场景下,可以将ReentrantLock与ReentrantReadWriteLock结合使用,实现更复杂的同步策略。例如:
java复制public class Cache<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final ReentrantLock evictionLock = new ReentrantLock();
public V get(K key) {
rwl.readLock().lock();
try {
return map.get(key);
} finally {
rwl.readLock().unlock();
}
}
public void put(K key, V value) {
rwl.writeLock().lock();
try {
map.put(key, value);
} finally {
rwl.writeLock().unlock();
}
}
public void evictStaleEntries() {
if (evictionLock.tryLock()) {
try {
// 执行清理操作
rwl.writeLock().lock();
try {
// 实际清理逻辑
} finally {
rwl.writeLock().unlock();
}
} finally {
evictionLock.unlock();
}
}
}
}
这种模式在读多写少且需要定期清理的场景中非常有用。
7.2 锁的层次结构与死锁预防
在设计使用多个锁的系统时,定义清晰的锁层次结构可以预防死锁:
java复制public class Account {
private final ReentrantLock lock = new ReentrantLock();
private int balance;
public void transfer(Account to, int amount) {
// 定义锁的获取顺序
Account first = this.hashCode() < to.hashCode() ? this : to;
Account second = this.hashCode() < to.hashCode() ? to : this;
first.lock.lock();
try {
second.lock.lock();
try {
if (this.balance >= amount) {
this.balance -= amount;
to.balance += amount;
}
} finally {
second.lock.unlock();
}
} finally {
first.lock.unlock();
}
}
}
通过统一锁的获取顺序,可以有效避免死锁。
7.3 锁与内存可见性
ReentrantLock不仅提供互斥功能,还保证了内存可见性。与synchronized类似,ReentrantLock的加锁和解锁操作会建立happens-before关系:
- 线程A释放锁前的所有写操作对线程B获取锁后都是可见的
- 这种可见性保证是通过内存屏障实现的
这意味着不需要额外使用volatile来保证共享变量的可见性,只要所有访问都在锁的保护下即可。
8. 替代方案与选择建议
8.1 何时选择ReentrantLock
考虑使用ReentrantLock的场景:
- 需要可中断的锁获取
- 需要超时获取锁
- 需要公平性保证
- 需要多个条件变量
- 需要尝试获取锁而不阻塞
- 需要更细粒度的锁控制
8.2 何时选择synchronized
优先使用synchronized的场景:
- 简单的同步需求
- 锁获取和释放的代码块明显且简单
- 不需要ReentrantLock的高级特性
- 性能不是关键考量因素
8.3 其他替代方案
- ReentrantReadWriteLock:读多写少的场景
- StampedLock:Java 8引入,提供乐观读模式
- 原子变量:简单的计数器等场景
- 并发集合:如ConcurrentHashMap,内部已经处理好同步
在实际项目中,我通常会先使用synchronized,只有当它无法满足需求时才会考虑ReentrantLock。这种保守的选择策略可以减少复杂性,避免过早优化。
