直入主题
可重入锁ReentrantLock中的实现方式有两种:公平锁和非公平锁,今天我们主要研究下公平锁
1、公平锁
调用lock.lock()时,会调用sync.lock(),具体来看下源码:
ReentrantLock:
public void lock() {
sync.lock();
}
FairSync:
final void lock() {
acquire(1);
}
AbstractQueuedSynchronizer:
public final void acquire(int arg) {
// 首先在tryAcquire(1)中尝试获取锁
// true:获取成功直接返回
// false:获取失败,继续调用addWaiter(node)加入同步队列,入队后,执行acquireQueued(node,arg)
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
这里讨论的是公平锁的实现,因此进入公平锁的tryAcquire(acquires)
protected final boolean tryAcquire(int acquires) {
// 首先获取当前线程
final Thread current = Thread.currentThread();
// 获取同步状态
int c = getState();
if (c == 0) {
// 同步状态为0(可以认为锁未被持有)
// 进入hasQueuedPredecessors()判断下是否需要排队 (我们重点分析的方法)
// 第一种情况 高并发场景下,t1、t2、t3同时执行到此处
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;
}
继续进入hasQueuedPredecessors()
public final boolean hasQueuedPredecessors() {
// 高并发场景下,t1、t2、t3同时执行到此处,首先CPU时间片切换至t1线程
// 此时AQS队列尚未初始化,即head=tail=null,
// 因此h!=t不成立(由于是短路与运算,h!=t不成立,所以&&后边的条件不会判断),
// 于是hasQueuedPredecessors()返回false,继续执行compareAndSetState(0,1)成功,
// 设置exclusiveOwnerThread为t1,t1获取到了锁
// 时间片切换到t2,同样此时AQS队列尚未初始化,h!=t不成立,hasQueuedPredecessors()返回false,
继续执行casState(0,1),这时t2会执行失败(因为此时锁的拥有者是t1,t1还未释放锁),那么t2就会继续执行
addWaiter(node),这时AQS队尾tail是null,所以会执行enq()进行初始化队列
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());
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
// t2执行到这里时,AQS完成了初始化,此时head=tail且不为null,这时时间片切换到t3,继续进入t3线程的执行逻辑分析
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
public final boolean hasQueuedPredecessors() {
// 时间片切换至t3,注意此时AQS队列的结构为head和tail同时指向一个虚拟节点(不为空),队列中只有一个
节点,因为t2完成了队列的头尾初始化后,还没来得及加入队列,CPU进行了时间片切换至t3
// 因此h!=t不成立,&&后边的条件不会执行,hasQueuedPredecessors()直接返回false
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());
}
基于t3的状态对可能出现的两种情况进行分析:
此时t3处于什么样的状态呢?
t3已经从hasQueuedPredecessors()退出来了得到false,
取反后准备执行casState()的状态(等待执行的状态,具体情况见以下两种情况)
1、时间片切换为t1,t1释放了锁,state置为0,时间片切换为t3,t3尝试casState是可以成功的,也就占有了锁
问题来了!t3居然比t2抢先占有了锁,这不是非公平的吗?
这里我是这么理解的,现在AQS队列中的情况是头尾节点初始化好了(也仅仅是初始化好了头尾节点,指向同一个节点),但是t2线程所在的节点尚未加入队列(CPU时间片切换时,AQS队列初始化好了,但t2尚未加入队列中,处于游离状态,这个时候t2所在的节点不算排队),所以t3不算插队,结合方法名来看hasQueuedPredecessors()意思就是说,队列中是否有处于排队中的前驱节点,有的话就老老实实去排队,没有的话可以尝试加锁
2、时间片切换为t2,t2将AQS队列初始化后且加入了队列
(也就是此时队列中的节点数是2个,头:虚拟节点、尾:t2线程所在的节点),
就进入到acquireQueued(),t2线程所在的节点记为n2,其前驱节点为head,因此会继续执行tryAcquire(arg),继续执行hasQueuedPredecessors():
此时AQS队列的head!=tail成立,继续判断head.next==null(不成立),继续判断s.thread != Thread.currentThread()(false,此时老二节点中的线程就是t2),因此hasQueuedPredecessors()方法返回false,取反后准备执行casState(0,1),此时上下文切换到了t1,t1释放了锁,state=0,上下文切换至t3,t3继续执行tryAcquire(1)中的compareAndSetState(0, acquires),t3就可能casState(0,1)成功,再切换回t2,t2casState(0,1)就会失败,也就是获取锁失败
(为什么?不是公平锁吗?t2已经在队列里了,t3居然还能抢到锁,希望大佬们帮忙解答下,小码农在此谢过!)