1. ReentrantLock锁机制概述
在Java并发编程中,ReentrantLock作为synchronized关键字的替代方案,提供了更灵活的锁控制机制。我第一次在实际项目中使用ReentrantLock时,就被它精细化的锁控制能力所吸引。与synchronized相比,ReentrantLock最显著的特点就是支持公平性选择,这也是我们今天要深入探讨的重点。
公平锁和非公平锁的本质区别在于获取锁的策略不同。公平锁就像银行排队办理业务,严格按照先来后到的顺序;而非公平锁则像地铁早高峰,谁先挤上去谁就先走。这种差异看似简单,但在高并发环境下会产生完全不同的性能表现。
重要提示:这里的"公平"特指获取锁的顺序公平性,而不是线程调度的公平性。即使使用非公平锁,一旦线程进入等待队列,唤醒顺序仍然是FIFO的。
2. 公平锁实现原理
2.1 公平锁的核心机制
公平锁的实现关键在于hasQueuedPredecessors()方法。这个方法会检查当前线程是否是等待队列中的第一个线程,如果不是,即使锁可用也会放弃获取机会。这种机制确保了严格的FIFO顺序。
在实际项目中,我曾遇到一个需要严格顺序处理的账单生成场景。使用公平锁后,即使在高并发情况下,账单也能按照请求到达的顺序依次处理,避免了顺序错乱的问题。
java复制// ReentrantLock.FairSync
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // 关键检查
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
2.2 公平锁的性能特点
公平锁的最大优势是保证了绝对的顺序性,但这是以性能为代价的。在我的压力测试中,公平锁的吞吐量通常比非公平锁低10-20%。这是因为:
- 每次锁释放时,必须唤醒队列中的第一个等待线程
- 新来的线程不能利用锁释放和线程唤醒之间的时间间隙
- 增加了上下文切换次数
3. 非公平锁实现原理
3.1 非公平锁的抢占策略
非公平锁的实现更加"激进",它提供了两次抢占机会:
- 在
lock()方法入口处直接尝试CAS获取锁 - 在
tryAcquire方法中再次尝试获取
这种设计使得新来的线程有可能"插队"成功,特别是在锁频繁切换的场景下。
java复制// ReentrantLock.NonfairSync
final void lock() {
if (compareAndSetState(0, 1)) // 第一次尝试
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(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;
}
}
// 重入逻辑...
}
3.2 非公平锁的性能优势
在我的性能测试中,非公平锁在高并发场景下表现优异。例如在一个模拟的电商秒杀系统中,使用非公平锁的QPS比公平锁高出约30%。这是因为:
- 减少了线程挂起和唤醒的次数
- 充分利用了锁释放和线程唤醒之间的时间窗口
- 降低了上下文切换的开销
但需要注意的是,非公平锁可能导致某些线程长时间获取不到锁(饥饿现象)。在实际项目中,我发现当锁持有时间较短且竞争不激烈时,这种问题很少出现。
4. 两种锁的对比与选择
4.1 特性对比
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取策略 | 严格FIFO | 允许插队 |
| 性能 | 较低 | 较高 |
| 饥饿现象 | 不会发生 | 可能发生 |
| 实现复杂度 | 较高 | 较低 |
| 适用场景 | 需要严格顺序 | 追求高吞吐 |
4.2 选型建议
根据我的项目经验,选型时需要考虑以下因素:
- 业务需求:如果业务逻辑对顺序有严格要求(如交易处理),考虑使用公平锁
- 性能要求:在高并发、高性能场景下,非公平锁通常是更好的选择
- 锁竞争程度:当竞争不激烈时,两种锁的表现差异不大
- 锁持有时间:持有时间越长,公平锁的性能劣势越明显
实际经验:在大多数情况下,非公平锁是更好的选择。JDK默认使用非公平锁也是基于这个考虑。只有在确实需要保证顺序性的场景下,才应该使用公平锁。
5. 实现细节与优化技巧
5.1 AQS队列管理
ReentrantLock的公平性实现依赖于AQS(AbstractQueuedSynchronizer)的队列管理。理解这一点对正确使用锁非常重要:
- 当线程获取锁失败时,会被封装成Node加入队列
- 队列是CLH锁队列的变体,使用双向链表实现
- 每个Node会自旋检查前驱节点的状态
5.2 重入机制
无论是公平锁还是非公平锁,都支持重入。这个特性在实际开发中非常有用:
java复制lock.lock();
try {
// 可以再次获取同一个锁
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
需要注意的是,重入次数必须与释放次数严格匹配,否则会导致其他线程无法获取锁。
5.3 性能优化实践
在我的项目中,通过以下优化显著提升了锁性能:
- 减少锁粒度:将大锁拆分为多个小锁
- 缩短临界区:只把必要的代码放在锁内
- 使用tryLock:避免长时间等待
- 锁分离:读写锁分离等策略
6. 常见问题与解决方案
6.1 死锁问题
在使用ReentrantLock时,我曾遇到过典型的死锁场景:
java复制// 线程1
lockA.lock();
try {
lockB.lock();
// ...
}
// 线程2
lockB.lock();
try {
lockA.lock();
// ...
}
解决方案:
- 统一锁的获取顺序
- 使用tryLock设置超时时间
- 使用锁的层次结构
6.2 性能瓶颈
在高并发环境下,锁可能成为性能瓶颈。通过JProfiler分析,我发现以下优化点:
- 使用更轻量的同步机制(如CAS)
- 考虑无锁数据结构
- 减少锁的争用(如使用ThreadLocal)
6.3 内存可见性
ReentrantLock的内存语义与synchronized相同,都能保证可见性。但在实际项目中,我遇到过因误用导致的问题:
java复制// 错误示例
if (condition) {
lock.lock();
try {
// 可能看到过期的值
} finally {
lock.unlock();
}
}
正确的做法是,所有对共享变量的访问都应该在锁的保护范围内。
7. 实际应用案例分析
7.1 订单处理系统
在一个分布式订单处理系统中,我们使用公平锁保证了订单处理的顺序性。核心代码如下:
java复制public class OrderProcessor {
private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
public void processOrder(Order order) {
lock.lock();
try {
// 顺序处理订单
validate(order);
persist(order);
notify(order);
} finally {
lock.unlock();
}
}
}
这种设计确保了先到的订单先处理,避免了优先级反转问题。
7.2 缓存更新机制
在高性能缓存系统中,我们使用非公平锁来提高吞吐量:
java复制public class CacheManager {
private final ReentrantLock lock = new ReentrantLock(); // 非公平锁
private Map<String, Object> cache = new HashMap<>();
public void updateCache(String key, Object value) {
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
cache.put(key, value);
} finally {
lock.unlock();
}
}
}
}
使用tryLock避免了线程阻塞,同时非公平策略提高了并发性能。
8. 高级话题与扩展思考
8.1 与synchronized的对比
虽然ReentrantLock功能更强大,但在简单场景下,synchronized仍然是更好的选择:
- synchronized更简洁,不易出错
- JVM对synchronized有更多优化
- 在低竞争场景下性能相当
8.2 条件变量的使用
ReentrantLock的条件变量(Condition)提供了更灵活的线程通信机制:
java复制private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void await() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void signal() {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
这种机制在生产者-消费者模式中非常有用。
8.3 锁的监控与调试
在实际项目中,我经常使用以下方法监控锁状态:
- 使用
getQueueLength()监控等待线程数 - 使用
isHeldByCurrentThread()调试锁持有情况 - 使用Thread dump分析锁争用
这些技术对诊断复杂的并发问题非常有帮助。