上一节我们在分析AQS源码时知道,AQS内部维护了一个同步状态位state
和一个虚拟的同步队列
来实现锁的争抢,其实除此以外还维护了一个条件队列。这个条件队列也不神秘,我们在开发中或多或少都使用过JUC的Conditon
,用来做多个线程之间的等待唤醒,它的底层就是我们今天要说的AQS的条件队列。
我们先用一张图来看下AQS的内部结构:
上一节我们主要分析了AQS的CLH等待队列
,今天我们着重来分析下条件队列的源码。
我们先看下AQS的ConditionObject
类的结构:
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/**
* 条件队列的第一个节点
*/
private transient Node firstWaiter;
/**
* 条件队列的最后一个节点
*/
private transient Node lastWaiter;
//重写Conditon接口的方法
{
//等待
void await() throws InterruptedException;
//.......
//唤醒
void signal();
void signalAll();
}
}
复制代码
从类结构上不难看出,它也是使用Node
作为队列的节点,前面我们分析AQS源码时已经看过了Node
的组成结构,这里就不再看了。
我们以一个常见的面试题引出我们今天要讲的条件队列
,就是3个线程顺序打印A-B-C, 我们先看下演示代码:
/**
* @author qiuguan
* @date 2022/12/10 22:48:23 星期六
*
* 控制线程打印A - B - C
*/
public class LockConditionDemo {
private final Lock lock = new ReentrantLock();
/**
* lock.newCondition()就是创建了一个上面的 ConditionObject 对象
* 一共创建了3个 ConditionObject 对象
*/
private final Condition c1 = lock.newCondition();
private final Condition c2 = lock.newCondition();
private final Condition c3 = lock.newCondition();
private int flag = 0;
public void printA(){
lock.lock();
try {
while (flag != 0) {
try {
c1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("--------------> A");
flag++;
c2.signal();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (flag != 1) {
try {
c2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("--------------> B");
flag++;
c3.signal();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (flag != 2) {
try {
c3.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("--------------> C");
flag = 0;
c1.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockConditionDemo lc = new LockConditionDemo();
new Thread(lc::printC, "tc").start();
new Thread(lc::printB, "tb").start();
new Thread(lc::printA, "ta").start();
}
}
复制代码
从代码中我们可以清晰的知道,tc,tb,ta 三个线程分别调用各自的方法,然后各自执行lock.lock()
方法,其中只有一个线程能获得锁,获取不到锁的线程将会进入等待队列。我们建设tc线程率先获得锁,然后tb线程,ta线程先后进入等待队列。
tc线程优先获得锁
public void printC(){
//tc线程过来获取锁
lock.lock();
try {
while (flag != 2) {
try {
c3.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("--------------> C");
flag = 0;
c1.signal();
} finally {
lock.unlock();
}
}
复制代码
tc线程先执行,它会调用printC
方法,进而执行lock.lock()
方法,这个我们前面分析AQS的时候知道,tc线程进来会将同步状态位state
从0修改为1,表示获取锁,然后继续往下执行,由于flag != 2
条件成立,所以将会执行c3.await()
的逻辑,到这里就要带领我们走进AQS的条件队列。接下来我们就看下c3.await()
具体都做了什么?我们直接看源码:
public final void await() throws InterruptedException {
//如果线程是中断的,则抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//1. 创建一个条件队列的节点
Node node = addConditionWaiter();
//2. 释放锁,核心逻辑就是lock.unlock()方法的内容
int savedState = fullyRelease(node);
int interruptMode = 0;
//3. 判断是否在同步队列中,也就是前面说的CLH同步队列
while (!isOnSyncQueue(node)) {
//不在同步队列中,挂起
LockSupport.park(this);
//被唤醒后接着往下执行,然后checkInterruptWhileWaiting(node))方法会将当前
//node加入到同步队列中(入队:入的是同步队列)
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//acquireQueued(node, savedState)这个方法就比较熟悉了,前面分析AQS的时候着重说过
//它就是加入同步队列后,等待被唤醒,然后去抢锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
复制代码
这个方法在
AbstractQueuedSynchronizer
(AQS)类中
我们先看下创建条件队列节点的方法:addConditionWaiter()
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//创建一个Node节点,waitStatus 是 Node.CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//整个逻辑就是尾插法插入到条件队列的尾部
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
复制代码
我们用一张图来看下入条件队列:
我们用一张图来描述下tc线程的await()方法都干了什么
从图中我们知道,tc线程加入条件队列
后,会唤醒等待队列
中等待的线程,然后将自己挂起。然后接下来我们看下唤醒tb线程(FIFO)后,tb线程都做了什么?
唤醒tb线程
public void printB(){
lock.lock();
try {
while (flag != 1) {
try {
c2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("--------------> B");
flag++;
c3.signal();
} finally {
lock.unlock();
}
}
复制代码
tb线程获得锁后,将从lock.lock()
继续往下执行,然后来到c2的await()
方法中,那这就比较熟悉了,因为它和tc线程是一样的。
- 首先tb线程要加入条件队列(注意:这个条件队列和tc线程不是同一个,因为他们是分别newCondition获得的)
- 然后我们再看下tb线程的
awiat()
方法做了什么?
从图中我们很清晰的知道,tb线程加入条件队列
后,会唤醒等待队列
中等待的线程,然后将自己挂起。然后接下来我们看下唤醒ta线程后,ta线程都做了什么?
唤醒ta线程
public void printA(){
lock.lock();
try {
while (flag != 0) {
try {
c1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("--------------> A");
flag++;
c2.signal();
} finally {
lock.unlock();
}
}
复制代码
ta线程获得锁后,将从lock.lock()
继续往下执行,由于它不满足while条件,所以它先打印 "--------------> A",然后将执行c2.signal() 方法,那么接下来我们就看下这个方法都做了什么?
注意:是c2的signal()方法
public final void signal() {
//判断当前的线程是否为持有锁的线程,tb线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//找到条件队列中的第一个first节点
Node first = firstWaiter;
if (first != null)
//唤醒
doSignal(first);
}
复制代码
这里的firstWaiter节点是封装了tb线程的Node节点
我们继续跟进去看下:
private void doSignal(Node first) {
//整个do逻辑是将节点从头到尾一个一个的从条件队列中移除
do {
//这里的逻辑就是看下条件队列是不是只有一个节点
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
//if条件里已经完成了赋值操作,将下一个节点推到了首节点
first.nextWaiter = null;
//重点是transferForSignal(first) 方法
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
复制代码
我们接着看下transferForSignal(first)
方法:
这个方法的逻辑就是将条件队列的节点转移到同步队列中
final boolean transferForSignal(Node node) {
/*
* 如果CAS修改失败,说明这个节点已经被取消了
* 一旦被取消了,就重新执行上面的do-while循环
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* CAS状态修改成功,将条件状态修改为了初始值0
*/
//入队:入的是"等待队列"的队 ,并返回前驱节点
Node p = enq(node);
int ws = p.waitStatus; //查看前驱节点的状态,默认值0
//CAS 将前驱节点的状态修改为-1(具有唤醒下一个节点的责任),如果成功,返回true,
// 如果失败,返回false,取反后则将进入if判断的内部逻辑,也就是唤醒线程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
复制代码
这个方法执行完
- 条件队列变成了这样:
- 同步队列就变成了这样:
然后退出c2.signal()
法方法,然后执行printA()
方法的lock.unlock();
,这个方法就比较熟悉了,前面讲AQS基础的时候有说过,这里我们在简单看下:
public final boolean release(int arg) {
//ta线程释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒后继节点
unparkSuccessor(h);
return true;
}
return false;
}
复制代码
在这个方法中,将会调用
LockSupport.unpark(s.thread);
唤醒tb线程
我们看下之前tb线程park的地方:
然后tb线程将会继续往下执行,由于它已经在同步队列中了,所以将会退出while循环,然后执行acquireQueued
方法,这个方法我们也比较熟悉了,就是尝试获取锁,如果获取不到就继续在同步队列中park。
由于tb线程此时可以获取锁,所以它将不会再继续park,继续往下执行业务代码,将会打印"--------------> B"
然后tb线程继续执行c3.signal()
方法,继续去唤醒tc线程,然后打印"--------------> C"
好了,关于AQS的条件队列就分析到这里吧,可能阅读起来比较复杂,限于作者水平有限,不能更好的表达,敬请谅解。
作者:秋官
原文链接:https://juejin.cn/post/7182869415321403449