AQS(AbstractQueuedSynchronizer)本身既可以用来实现 公平锁,也可以用来实现 非公平锁,它提供了支持两者的基础机制。在 Java 并发框架中,具体是通过 ReentrantLock
等实现类来区分公平和非公平锁。
1. 公平锁与非公平锁的定义
- 公平锁(Fair Lock):线程获取锁的顺序与它们请求锁的顺序一致。先请求锁的线程将优先获得锁,后请求的线程需要排队等待,遵循先进先出(FIFO)规则。
- 非公平锁(Non-Fair Lock):线程获取锁的顺序与请求锁的顺序无关。任何线程都有可能在锁释放时立刻尝试抢占锁,即使其他线程已经在排队等待。
2. AQS 如何实现公平锁和非公平锁
AQS 本身并不直接决定公平或非公平,而是提供了基础的锁请求排队机制,具体的公平性策略是在子类中实现的。例如,在 ReentrantLock
中,可以通过构造函数选择使用公平锁或非公平锁。
2.1 公平锁的实现
在 公平锁 中,每个线程必须按顺序获取锁,这就要求线程在获取锁时需要首先检查 等待队列 中是否有其他线程。如果等待队列中有其他线程,则当前线程不能直接获取锁,必须进入队列等待。
实现公平锁的核心在于重写 AQS
的 tryAcquire()
方法,保证线程按照队列的顺序获取锁。
公平锁的工作流程:
- 线程尝试获取锁时,首先通过
hasQueuedPredecessors()
方法检查当前等待队列中是否有其他线程。 - 如果有其他线程等待,则当前线程不能直接获取锁,必须进入等待队列。
- 如果没有其他线程等待,线程可以继续通过
CAS
操作尝试获取锁。
ReentrantLock
的公平锁实现:
javaCopy codeprotected final boolean tryAcquire(int acquires) {
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;
setState(nextc);
return true;
}
return false;
}
hasQueuedPredecessors()
方法用于判断当前线程前是否有排队的线程,如果有的话当前线程将进入队列等待。
2.2 非公平锁的实现
非公平锁 不需要遵循队列顺序,它允许线程在任何时候直接尝试获取锁,甚至可能“插队”在等待队列前面。如果当前锁是空闲的,线程不必检查等待队列,直接通过 CAS
操作尝试获取锁。
非公平锁的工作流程:
- 线程首先直接尝试获取锁,不考虑等待队列中的其他线程。
- 如果锁是空闲的,当前线程立即获取锁,不论是否有其他线程在等待。
- 如果锁被占用,线程将进入等待队列,直到锁释放。
ReentrantLock
的非公平锁实现:
javaCopy codeprotected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
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;
setState(nextc);
return true;
}
return false;
}
在非公平锁中,线程通过 nonfairTryAcquire()
方法直接尝试获取锁,而不会检查队列中的其他线程是否在等待,这种方式提高了锁的竞争效率。
3. 公平锁和非公平锁的区别
3.1 公平性
- 公平锁:线程按照请求锁的先后顺序获取锁,避免了线程“插队”的现象,确保先请求的线程先得到锁。这适合需要强顺序性的场景,但会导致线程上下文切换频繁,性能略有损耗。
- 非公平锁:线程直接尝试获取锁,无需等待队列中排在前面的线程释放锁,可能会出现“插队”的情况。这种锁的吞吐量较高,因为允许线程更快地获得锁,减少了排队的开销,但可能导致某些线程长期得不到锁,产生 线程饥饿 问题。
3.2 性能
- 公平锁:由于需要检查等待队列,公平锁会增加额外的上下文切换开销,导致性能下降,尤其在高并发场景下。
- 非公平锁:非公平锁减少了上下文切换的开销,性能更高,特别是在并发量大的情况下,锁的获取速度更快。
3.3 场景适用性
- 公平锁:适用于需要严格控制访问顺序的场景。例如:一些金融系统、银行交易系统中,需要保证事务的严格顺序性。
- 非公平锁:适用于追求性能、并发高的场景。例如:大多数 Web 应用、缓存系统等,允许线程快速获取锁,以提高吞吐量。
4. 公平锁和非公平锁的选择
4.1 公平锁适用场景
- 需要严格按照请求顺序获取锁的业务场景。
- 长时间运行的任务可能会受到锁竞争影响的情况。
公平锁可以避免某些线程长期得不到锁(线程饥饿问题),但可能会降低吞吐量。适合对顺序性要求高的场景,例如:
- 银行转账系统。
- 订单处理系统。
- 需要保证资源分配顺序的系统。
4.2 非公平锁适用场景
- 追求高吞吐量、需要极大提升性能的业务场景。
- 线程饥饿不是问题,允许线程快速抢占锁的场景。
非公平锁适合于高并发、性能敏感的场景,例如:
- Web 服务器处理大量并发请求。
- 缓存系统中,大量线程频繁获取和释放锁。
- 高性能计算或需要减少上下文切换的系统。
5. 总结
- 公平锁:按照线程请求顺序获取锁,避免线程饥饿,适合需要严格控制访问顺序的场景,但性能相对较低。
- 非公平锁:允许线程插队,性能更高,适合高并发场景,但可能导致线程饥饿。
在 Java 中,ReentrantLock
通过构造函数提供了公平锁和非公平锁的选择:
javaCopy codeReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock nonFairLock = new ReentrantLock(false); // 非公平锁
选择哪种锁,主要取决于应用场景的需求:如果系统需要严格保证公平性,选择公平锁;如果需要更高的性能和吞吐量,选择非公平锁。