AQS
(AbstractQueuedSynchronizer)是 Java 并发包(java.util.concurrent
)中的一个重要基础类,它用来实现同步器的核心机制,比如 ReentrantLock
、Semaphore
、CountDownLatch
等。AQS
提供了一个先进的队列(FIFO 队列)来管理竞争线程的排队和唤醒,并通过状态(state
)来表示锁的持有状态。
在 AQS
中,公平锁和非公平锁是两种不同的锁实现方式:
-
公平锁:按照线程请求锁的顺序来分配锁,先请求的线程先获取锁,避免“饥饿”现象。
-
非公平锁:允许“插队”,新来的线程可以直接尝试抢占锁,性能通常比公平锁好,但可能会导致某些线程长期得不到锁。
1. 公平锁和非公平锁的区别
- 公平锁:线程获取锁的顺序严格按照它们请求的顺序,FIFO(先入先出)队列中最早等待的线程优先获得锁。
- 非公平锁:线程尝试直接获取锁,而不管队列中是否有其他线程在等待。如果锁空闲,当前线程可以立即获取锁而不需要排队。
2. AQS 公平锁和非公平锁的实现
在 AQS
中,锁的公平性主要体现在锁的获取逻辑上。具体来说,AQS
提供了两个核心的锁实现类:
- 公平锁:
ReentrantLock
的公平实现通过FairSync
类来实现。 - 非公平锁:
ReentrantLock
的非公平实现通过NonFairSync
类来实现。
ReentrantLock
通过 AQS
提供的 acquire
和 tryAcquire
方法实现锁的获取逻辑,而公平锁和非公平锁的核心区别就在于 tryAcquire
方法的实现。
2.1 非公平锁的实现
非公平锁允许线程直接尝试获取锁,而不考虑队列中是否有其他等待的线程。这种机制可以减少线程切换的开销,提升性能。
关键点:
- 抢占式获取锁:非公平锁允许新来的线程直接尝试获取锁,而不管队列中是否有其他线程在排队。
- 性能更高:由于直接进行抢占,线程切换次数较少,性能通常比公平锁更好。
非公平锁的 tryAcquire()
实现:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 非公平的锁获取逻辑
@Override
final boolean tryAcquire(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;
}
}
NonfairSync
是非公平锁的具体实现类。- 当锁的状态为 0 时,表示锁是空闲的,当前线程将直接尝试通过
compareAndSetState(0, acquires)
获取锁,而不检查队列中是否有其他线程在等待。 - 如果当前线程已经持有锁,则可以重入锁,并通过增加
state
的值来表示重入次数。
2.2 公平锁的实现
公平锁的实现严格按照线程请求锁的顺序来分配锁。即使当前线程能够立即获取锁,它也必须先检查队列中是否有其他等待的线程,只有在队列为空或者当前线程是队列中的第一个时,才能获取锁。
关键点:
- FIFO 顺序:公平锁会检查队列中是否有其他线程在等待,只有队列头的线程才能获取锁。
- 避免插队:公平锁保证了“先到先得”的顺序,防止某些线程长时间得不到锁。
公平锁的 tryAcquire()
实现:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 公平的锁获取逻辑
@Override
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) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
FairSync
是公平锁的具体实现类。- 当锁是空闲的(
c == 0
),公平锁首先通过hasQueuedPredecessors()
方法检查队列中是否有其他等待的线程。如果队列为空,或者当前线程是等待队列中的第一个线程,才允许当前线程获取锁。 - 如果当前线程已经持有锁,则可以重入锁,并通过增加
state
的值来表示重入次数。
2.3 hasQueuedPredecessors() 方法
在公平锁的实现中,hasQueuedPredecessors()
是判断当前线程是否可以获取锁的关键方法。
public final boolean hasQueuedPredecessors() {
// 返回 true 表示当前线程前面有等待的线程
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
- 该方法返回
true
,表示当前线程前面有其他线程在等待队列中,因此当前线程不能获取锁。 - 该方法的作用是检查等待队列中的第一个节点是否是当前线程,如果不是,说明当前线程前面还有其他线程在等待。
3. 公平锁与非公平锁的性能差异
- 非公平锁性能更高:由于非公平锁允许新线程直接尝试获取锁,减少了上下文切换的开销,因此在高竞争环境下,非公平锁的性能通常优于公平锁。
- 公平锁避免线程饥饿:公平锁保证了“先到先得”的顺序,适合那些需要严格控制线程执行顺序的场景,避免了某些线程长期得不到锁的情况。
- 选择何种锁:大多数情况下,非公平锁的性能更好,因为它减少了线程切换的开销。但如果某些线程可能会因为其他线程频繁“插队”而长期得不到锁,则应该选择公平锁。
4. 实际应用中的选择
- ReentrantLock:Java 提供的
ReentrantLock
类支持两种模式:公平模式和非公平模式。默认情况下,ReentrantLock
是非公平锁,但可以通过构造函数控制锁的公平性。
构造公平锁与非公平锁的例子:
// 创建一个非公平的 ReentrantLock(默认)
ReentrantLock nonFairLock = new ReentrantLock();
// 创建一个公平的 ReentrantLock
ReentrantLock fairLock = new ReentrantLock(true);
- 默认非公平:
ReentrantLock
默认是非公平锁,以提高性能。 - 公平锁通过构造参数控制:当你需要公平锁时,可以在构造
ReentrantLock
时传入true
参数。
5. 总结
- 公平锁通过严格的排队机制,保证了线程按照请求顺序获取锁,避免了线程饥饿现象;但由于需要频繁检查等待队列,可能会导致性能下降。
- 非公平锁允许线程直接尝试获取锁,不必排队,这种机制减少了线程切换的开销,因此性能通常优于公平锁,但可能导致某些线程长时间得不到锁。
在实际开发中,非公平锁通常是首选,除非有明确的需求(如希望严格控制执行顺序或避免线程饥饿现象),才使用公平锁。