之前一直以为公平锁和非公平锁的实现是一种非常复杂的实现机制,直到今天看了源码
在ReentrantLock中,我们默认使用的就是非公平锁
下面就是其源码,其实实现非常的简单
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
// 非公平锁,在进行lock的时候,不管等待队列中是否有线程先尝试去获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 获取不成功才将自己加入到等待队列之中
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
// nonfairTryAcquire方法是Sync的方法,就是简单判断状态,如果状态为0就尝试获取
return nonfairTryAcquire(acquires);
}
}
公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
// 一进来没有马上尝试去获取锁
acquire(1);
}
/**
* Sync会回调该方法来尝试获取锁
* 返回true表示获取锁成功
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// c等于0,说明当前的锁没有被占用
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;
}
}
从源码我们就能非常容易的看出公平锁和非公平锁的区别了,非公平锁可能会进行插队。
假如现在一个线程A获取到了锁,马上准备释放锁,一个线程B在等待队列中,然后有一个线程C马上准备来获取锁。
线程A释放锁的同时就会去唤醒后面的线程B,但是B被唤醒的这个过程比较花费时间,需要从 挂起->唤醒,在这个时间之内,可能C已经获取到这个锁了,因为C本身就是在运行状态。所以这就出现了不公平了。
在非公平的状态下,新来的线程在入队之前会尝试抢一次锁,如果失败了就会乖乖进入队列,一旦进入队列是不能再次出来抢的,只能等待队列一个一个地执行完毕(进入到了队列中的线程就公平了,先进入队列的一定比后进入的先获取到锁)。所谓不公平是指新来的线程会不会在入队之前尝试「野蛮」地抢锁,公平的时候是不会,但是非公平的时候是会的。
问题分析
非公平锁的最大问题就是可能存在线程饥饿问题,如果一个线程反复快速的获取锁,那么他一直就能得到这个锁,因为他处于运行状态,去获取锁比等待队列中的线程更快,等待队列中的线程需要经过挂起 -> 唤醒的状态变化。最终会导致等待队列中的线程饥饿,ReentrantLock和默认是非公平锁,synchronized也是非公平锁
当线程A释放锁时,线程B将经历从 挂起->唤醒 的线程调度过程,线程调度非常耗时。
在线程B的 挂起->唤醒 阶段:
- 如果采用非公平策略,那么线程C可以立即获取锁,线程C使用完并释放锁后,线程B可能才刚唤醒完成;此时线程B又可以去获取锁,这样线程B和线程C的效率都得到提升,系统吞吐量提升;
- 如果采用公平策略,线程C即使可用,也要等到线程调度完成,整个系统的吞吐量降低。
因此,当线程持有锁的时间相对较长或者线程请求锁的平均时间间隔较长时,可以考虑使用公平策略。此时线程调度产生的耗时间隔对总体的影响会较小。