之前讲过独占共享模式下Node节点的waitStatus信号量还有一个CONDITION = -2;没有说,并且AQS中还有一个ConditionObject内部类没有提到和条件队列下使用到的一些方法
static final class Node {
/**
* 标记节点为独占模式
*/
static final Node EXCLUSIVE = null;
/**
* 出现异常,中断引起的,需要废弃的node即节点. 中断一般是手动,程序异常通常是代码运行中问出题
* 在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待
* */
static final int CANCELLED = 1;
/**
* 可被唤醒
* 同步队列需要通道的状态
* 后继节点的线程处于等待状态,而当前的节点如果释放了同步状态或者被取消,
* 将会通知后继节点,使后继节点的线程得以运行。
*/
static final int SIGNAL = -1;
/**
* 条件等待
* 条件队列需要用到的状态
* 节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,
* 该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
*/
static final int CONDITION = -2;
/**
* 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
* 使用CAS更改状态,volatile保证线程可见性,高并发场景下,
* 即被一个线程修改后,状态会立马让其他线程可见。
*/
volatile int waitStatus;
/**
* 节点同步状态的线程
*/
volatile Thread thread;
/**
* PS:条件队列Node节点的下一个指针(单向链表)头尾指针在@See ConditionObject
* 构建条件队列使用到的参数,条件队列必须是独占的
*
* 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量,
* 也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段。
*/
Node nextWaiter;
}
/**
* ConditionObject 的作用是构建条件队列的头尾指针,以及两个重要的方法
*/
public class ConditionObject implements Condition {
/**
* 条件队列头指针
*/
private transient Node firstWaiter;
/**
* 条件队列尾部指针
*/
private transient Node lastWaiter;
//条件队列的唤醒
public final void signal() {}
//条件队列的阻塞
public final void await() throws InterruptedException {}
//其他一些用到的重要方法
final boolean transferForSignal(Node node)
}
上面是构建条件队列和使用条件队列的关键处,条件队列的作用简单说可以是用来作为CLH队列的中间过度的,如果阻塞队列满了,就没办法put了,那么put的线程就会先放入条件队列阻塞。当被触发条件调用signal后,条件队列的节点全部移动到CLH队列,并且唤醒。
通过构造器,认识阻塞队列的内部结构
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity]; // 阻塞队列容量大小
lock = new ReentrantLock(fair); // lock锁对象->clh队列
notEmpty = lock.newCondition(); //条件对象->条件队列
notFull = lock.newCondition(); //条件对象-条件队列
}
用消费者和生产者的角色描述简单的执行流程
/**
* 生产者put
* @param e
* @throws InterruptedException
*/
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 获取锁,如果中断抛出异常
lock.lockInterruptibly();
try {
// 容量满了
while (count == items.length)
//由于满足条件无法在放入元素,当前线程入条件队列,并且释放锁,然后唤醒CLH队列,然后当前线程阻塞,直到当前节点被加入到同步队列中,然后死循环在等待获取锁哪里
notFull.await();
// 放入item数组,唤醒消费者条件队列的线程
enqueue(e);
} finally {
lock.unlock();
}
}
/**
* 加入条件队列等待,条件队列入口
*/
public final void await() throws InterruptedException {
//中断状态检测,如果当前线程被中断则直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
// 构建waitStatus=Node.condition的节点并且加入条件队列
Node node = addConditionWaiter();
//释放掉put,take加的锁,唤醒CLH队列第一个节点
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判断节点是否在同步队列?不再则一直阻塞,或者中断退出循环。 这里是因为items容器每次添加删除完都会调用ConditionObject.signal方法会把条件队列的节点添加到CLH队列中。
while (!isOnSyncQueue(node)) {
// 节点在条件队列中会一直阻塞在这里
LockSupport.park(this);
//如果是中断唤醒的也跳出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//走到这里说明节点已经条件满足被加入到了同步队列中或者中断了
//这个方法很熟悉吧?就跟独占锁调用同样的获取锁方法,从这里可以看出条件队列只能用于独占锁,这里加完锁,在外面unlock释放
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//走到这里说明已经成功获取到了独占锁,接下来就做些收尾工作
//删除条件队列中被取消的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//根据不同模式处理中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 加锁
lock.lockInterruptibly();
try {
// 容器空了,消费者线程无法消费
while (count == 0)
// 和put一样同一个await方法,里面入条件队列,释放掉这里加的锁,然后阻塞在while循环,等待enqueue里的signal方法唤醒去获取锁
notEmpty.await();
return dequeue();
} finally {
// 释放锁
lock.unlock();
}
}
private E dequeue() {
// 返回items末尾元素
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
// 把生产者(notFull)这条条件队列放到CLH队列中
notFull.signal();
return x;
}
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 拿到条件队列头指针
Node first = firstWaiter;
if (first != null)
// 从头唤醒条件队列(入CLH队列,unpark线程),然后在await中了while循环判断中满足条件跳出循环。
doSignal(first);
}
/**
* 该方法就是把一个有效节点从条件队列中删除并加入同步队列
* 如果失败则会查找条件队列上等待的下一个节点直到队列为空
* @param first
*/
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
//将节点加入同步队列
final boolean transferForSignal(Node node) {
//修改节点状态,这里如果修改失败只有一种可能就是该节点被取消,具体看上面await过程分析
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//该方法很熟悉了,跟独占锁入队方法一样,不赘述
Node p = enq(node);
//注:这里的p节点是当前节点的前置节点
int ws = p.waitStatus;
//如果前置节点被取消或者修改状态失败则直接唤醒当前节点
//此时当前节点已经处于同步队列中,唤醒会进行锁获取或者正确的挂起操作
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
太大了,不好截取
https://www.processon.com/diagraming/5f5e3f28f346fb47ca9fa7bf
前言和AQS的条件队列的结构,差不多是一个简单面上流程的总结,再复杂点,多线程运行环境下的流程,可以看下上面图解。我觉得条件队列是AQS中最难的一块,因为很多代码涉及到多个线程切换的场景,但是学好了很有用,生产消费场景基本都是用这一套思路。