1. ReentrantLock锁的核心价值与应用场景
在Java多线程编程中,锁机制是保证线程安全的基石。相比传统的synchronized关键字,ReentrantLock提供了更灵活、更强大的线程控制能力。我第一次在生产环境使用ReentrantLock是在一个高并发的订单处理系统中,当时synchronized已经无法满足复杂的锁超时和中断需求。
ReentrantLock属于java.util.concurrent.locks包下的显式锁,它的核心特点包括:
- 可重入性:线程可以重复获取已经持有的锁
- 公平性选择:支持公平锁和非公平锁两种模式
- 锁等待可中断:支持lockInterruptibly()方法
- 尝试获取锁:提供tryLock()方法
- 条件变量:支持多个Condition对象
重要提示:虽然ReentrantLock功能强大,但必须手动释放锁,否则会导致死锁。建议始终在finally块中释放锁。
2. ReentrantLock底层实现原理剖析
2.1 AQS同步器架构
ReentrantLock的核心是基于AbstractQueuedSynchronizer(AQS)实现的。AQS使用一个volatile int类型的state变量来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
java复制// ReentrantLock中同步器的简化实现
abstract static class Sync extends AbstractQueuedSynchronizer {
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;
}
}
2.2 公平锁与非公平锁差异
ReentrantLock在构造时可以指定公平策略:
java复制// 非公平锁(默认)
ReentrantLock lock = new ReentrantLock();
// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);
公平锁与非公平锁的核心区别在于获取锁时的行为:
- 公平锁:严格按照FIFO顺序获取锁
- 非公平锁:允许插队,可能造成线程饥饿但吞吐量更高
在实际性能测试中,非公平锁的吞吐量通常比公平锁高出1-2个数量级,这也是默认采用非公平策略的原因。
3. ReentrantLock高级特性实战
3.1 可中断的锁获取
传统synchronized在等待锁时无法被中断,而ReentrantLock提供了lockInterruptibly()方法:
java复制ReentrantLock lock = new ReentrantLock();
try {
// 可响应中断的获取锁
lock.lockInterruptibly();
try {
// 临界区代码
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 处理中断异常
Thread.currentThread().interrupt();
}
这个特性在实现可取消任务时非常有用,比如在长时间等待的线程池任务中。
3.2 尝试获取锁与超时机制
ReentrantLock提供了灵活的tryLock方法:
java复制if (lock.tryLock()) {
try {
// 获取锁成功,执行临界区代码
} finally {
lock.unlock();
}
} else {
// 获取锁失败时的备用方案
}
// 带超时的尝试获取锁
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// ...
} finally {
lock.unlock();
}
} else {
// 超时处理逻辑
}
实战经验:在分布式锁降级方案中,本地锁经常使用tryLock配合超时时间,避免长时间阻塞影响系统响应。
3.3 条件变量(Condition)应用
Condition提供了比Object.wait/notify更精细的线程等待/唤醒控制:
java复制class BoundedBuffer {
final ReentrantLock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
这种模式在生产者-消费者场景中非常高效,相比Object的监视器方法,Condition可以创建多个等待队列,实现更精确的线程唤醒。
4. ReentrantLock性能优化与最佳实践
4.1 锁分段技术应用
在高并发场景下,可以采用锁分段技术减少竞争:
java复制class StripedMap {
// 锁数组
private final ReentrantLock[] locks;
// 存储数据的桶数组
private final Node[] buckets;
public StripedMap(int bucketCount, int lockCount) {
locks = new ReentrantLock[lockCount];
for (int i = 0; i < lockCount; i++) {
locks[i] = new ReentrantLock();
}
buckets = new Node[bucketCount];
}
private final int hash(Object key) {
return Math.abs(key.hashCode() % buckets.length);
}
public Object get(Object key) {
int hash = hash(key);
// 根据hash选择对应的锁
locks[hash % locks.length].lock();
try {
for (Node m = buckets[hash]; m != null; m = m.next)
if (m.key.equals(key))
return m.value;
return null;
} finally {
locks[hash % locks.length].unlock();
}
}
}
4.2 避免锁泄露的编程规范
使用ReentrantLock时必须遵循以下规范:
- 总是在try-finally块中释放锁
- 不要将锁对象暴露给不受信任的代码
- 避免在持有锁时调用外部方法(容易造成死锁)
- 锁的获取和释放必须成对出现
java复制// 反例:错误的锁使用方式
lock.lock();
try {
externalService.call(); // 危险!可能抛出异常或死锁
} finally {
lock.unlock();
}
// 正例:安全的锁使用方式
boolean acquired = false;
try {
acquired = lock.tryLock(5, TimeUnit.SECONDS);
if (acquired) {
// 执行临界区代码
}
} finally {
if (acquired) {
lock.unlock();
}
}
4.3 锁的监控与诊断
JDK提供了多种监控锁状态的方法:
java复制ReentrantLock lock = new ReentrantLock();
// 查询是否有线程在等待此锁
boolean hasQueuedThreads = lock.hasQueuedThreads();
// 获取等待队列长度
int queueLength = lock.getQueueLength();
// 查询当前线程是否持有锁
boolean isHeldByCurrentThread = lock.isHeldByCurrentThread();
// 查询锁是否被任何线程持有
boolean isLocked = lock.isLocked();
在生产环境中,可以结合这些方法实现锁的监控系统,当发现队列长度异常增长时及时报警。
5. ReentrantLock与synchronized的深度对比
5.1 功能特性对比
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 锁获取方式 | 显式调用lock()/unlock() | 隐式通过JVM管理 |
| 可中断性 | 支持lockInterruptibly() | 不支持 |
| 超时尝试 | 支持tryLock(timeout) | 不支持 |
| 公平锁 | 支持公平/非公平策略 | 只有非公平 |
| 条件变量 | 支持多个Condition | 只有一个等待队列 |
| 性能 | JDK6+优化后性能相当 | JDK6+优化后性能相当 |
| 代码灵活性 | 高 | 低 |
5.2 适用场景选择指南
优先使用synchronized的情况:
- 简单的同步块
- 不需要高级特性的场景
- 代码简洁性优先的项目
优先使用ReentrantLock的情况:
- 需要可中断的锁获取
- 需要尝试获取锁(tryLock)
- 需要公平锁策略
- 需要多个条件变量
- 需要获取锁的详细信息(如等待线程数)
性能提示:在JDK6及以后版本中,synchronized经过大量优化,在非竞争情况下性能与ReentrantLock相当。选择时应以功能需求为主,而非性能考虑。
6. 典型问题排查与解决方案
6.1 死锁问题诊断
使用jstack工具可以检测死锁:
bash复制jstack <pid> | grep -A 10 "deadlock"
预防死锁的建议:
- 按固定顺序获取多个锁
- 使用tryLock设置超时时间
- 避免在持有锁时调用外部方法
6.2 锁竞争性能问题
当发现系统吞吐量下降时,可以通过以下命令检查锁竞争:
bash复制jstack <pid> | grep -A 1 "parking to wait for"
优化方案:
- 减小临界区范围
- 使用锁分段技术
- 考虑使用读写锁(ReentrantReadWriteLock)替代
6.3 内存可见性问题
即使使用ReentrantLock也需要注意内存可见性:
java复制class VisibilityExample {
private int count;
private final ReentrantLock lock = new ReentrantLock();
void increment() {
lock.lock();
try {
count++; // 安全,因为锁保证了可见性
} finally {
lock.unlock();
}
}
int getCount() {
lock.lock(); // 必须加锁读取,否则可能读到旧值
try {
return count;
} finally {
lock.unlock();
}
}
}
7. 真实案例:电商库存服务中的锁应用
在某电商平台的库存服务中,我们使用ReentrantLock解决了以下问题:
场景: 秒杀活动期间,防止超卖和保证库存准确性
解决方案:
java复制public class InventoryService {
private final Map<Long, Integer> inventory = new HashMap<>();
private final Map<Long, ReentrantLock> itemLocks = new ConcurrentHashMap<>();
public boolean deductInventory(long itemId, int quantity) {
// 获取商品对应的锁(如果没有则创建)
ReentrantLock lock = itemLocks.computeIfAbsent(itemId, k -> new ReentrantLock());
lock.lock();
try {
Integer current = inventory.get(itemId);
if (current == null || current < quantity) {
return false;
}
inventory.put(itemId, current - quantity);
return true;
} finally {
lock.unlock();
}
}
}
优化点:
- 使用ConcurrentHashMap存储锁对象,避免为每个商品预先创建锁
- 细粒度锁只锁定特定商品,而不是整个库存表
- 在分布式环境下,还需配合分布式锁使用
这个方案在618大促期间成功支撑了每秒数万次的库存扣减请求,系统稳定运行零超卖。
