概念
其实锁的公平与否是人为的以某一参照作为界定,实际就是看设计者对于锁的公平与否是怎样考虑的。
以JDK提供的ReentrantLock
为例:
- 公平锁:当一个新的线程尝试获得锁的时候,会先去判断等待队列中是否有正在等待的线程,如果有,则进入等待等列等待,没有则可以尝试获得锁。
- 非公平锁:当一个新的线程尝试获得锁的时候,直接尝试获得锁,成功,则执行自己的操作逻辑,失败则进入等待队列中。
这样,公平锁的情况,能够完全保证获得锁的顺序就是队列的顺序,即FIFO
。而非公平锁的情况,由于新的线程直接尝试锁的获取,少了一个判断等待队列的状态,减少了线程唤醒的成本,所以效率会比较高。但是也很容易得出,在并发量高的情况下,等待队列中的线程可能永远无法获取锁,即俗称的饿死
。
纠错
一开始我认为对于等待队列中的线程,设计者会根据公平与否设计不同的出队列操作。即公平锁的时候,等待队列按照等待时间依次获得锁,先到先得,很公平;然后非公平锁就是等待队列中的线程是随机唤醒,体现一个谁运气好,谁获得锁,这很不公平。然而这个想法是错误
的。
在自己尝试写测试用例和解读ReentrantLock的源码后,就知道了这样认为是错误的。
源码简单解读
要解释JDK提供的各种锁,那就绕不开AQS(AbstractQueuedSynchronizer)
,内部维护了我们上面说的等待队列,该队列是CLH
(Craig, Landin, and Hagersten)设计的锁队列的一种变形,通常用作自旋锁的实现。AQS底层volatile
和CAS
实现。
在ReentrantLock中,有两个同步器类实现:FairSync
和NonfairSync
,前者就是公平锁的关键,后者是非公平锁实现的关键。他们共同继承自类Sync
,然后Sync
继承了AbstractQueuedSynchronizer
。实现公平与否的就在于它们各自重写的方法tryAcquire
FairSync#tryAcquire
可以看到通过hasQueuedPredecessors判断等待队列是否有线程等待
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;
}
NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
这里调用了父类的方法nonfairTryAcquire
Sync#nonfairTryAcquire
可以看到这里上来就设置锁状态,成功获得锁。
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;
}
当上面返回false,即获得锁失败,那么就会进入等待队列,后续公平锁与非公平锁的逻辑就是一样的,通过自旋的形式尝试获得锁。
可以看到这里判断了前驱节点是否为队头,所以不管是不是公平锁,出队列都是按照队列的顺序,FIFO。
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
若文章有误,或你有什么见解,欢迎留言交流指正。
原创不易,若有所帮助,欢迎点赞、收藏。