AQS之Condition条件队列源码解析

Condition的用法

上一篇文章分析了ReentrantLock抢锁、线程入队、释放锁的过程,这篇文章接着来看条件队列的应用。AbstractQueuedSynchronizer中的Condition表示条件队列,我们可以通过Condition实现线程通信,我们希望挂起线程,在满足某种条件时唤醒线程,例如很常见的实现生产-消费者模式,就可以通过条件队列实现。首先基于ReentrantLock来看一下条件队列的简单用法。

class ConditionTest {
    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    void awaitTest() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"线程挂起,等待被唤醒---");
            condition.await();
            System.out.println(Thread.currentThread().getName()+"线程被唤醒继续执行---");
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    void signalTest() {
        try {
            lock.lock();
            condition.signal();
            System.out.println(Thread.currentThread().getName()+"唤醒条件队列中的线程");
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args){
        JucApplicationTests tests = new JucApplicationTests();
        new Thread(tests::awaitTest).start();
        new Thread(tests::awaitTest).start();
        new Thread(tests::signalTest).start();
        new Thread(tests::signalTest).start();
    }
}

使用condition时必须获取相应的锁,也就是说await和signal操作是依赖ReentrantLock的,不管是挂起还是唤醒操作,都必须先持有锁。从语义上来说这点和Object中wait、notify、notifyAll很像,必须获取监视器锁才能进行操作。
我们来看一下ConditionObject类

Condition condition = lock.newCondition();

final ConditionObject newCondition() {
    return new ConditionObject();
}

public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;

    /**
     * Creates a new {@code ConditionObject} instance.
     */
    public ConditionObject() { }

    public final void signal() {
        ......
    }
    public final void signalAll() {
        ......
    }
    public final void await() throws InterruptedException {
        ......
    }
    ......
}
  1. ConditionObject中firstWaiter和lastWaiter分别代表条件队列的首尾节点,结合Node中nextWaiter属性构成单向链表,也就是我们说的条件队列。
  2. ConditionObject可以new多个,即可以同时存在多个条件队列。
  3. 条件队列和阻塞队列的节点都是Node实例,当有线程调用condition.signal会唤醒条件队列的队头,并转移到阻塞队列的队尾。
  4. 线程在调用condition.await方法后,会将自己包装成一个Node节点并加入条件队列的队尾,同时挂起线程。
  5. 条件队列与阻塞队列的区别:我们在分析ReentrantLock时提到阻塞队列的概念,线程在抢锁失败后,会进入阻塞队列排队等待,直到被前置节点唤醒。而条件队列是线程持有锁的前提下,线程执行到await进入条件队列挂起;条件队列中的线程将一直挂起直到有线程调用signal。

条件队列入队

首先看一下await方法

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //节点加入条件队列
    Node node = addConditionWaiter();
    //挂起前释放锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //如果节点不在阻塞队列中,则挂起线程,我们前面说过,节点会从条件队列转移到阻塞队列中
    while (!isOnSyncQueue(node)) {
        //线程在这里挂起,等待节点被唤醒
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}


private Node addConditionWaiter() {
    Node t = lastWaiter;
    //lastWaiter状态不是Node.CONDITION,说明取消排队了
    if (t != null && t.waitStatus != Node.CONDITION) {
        // 如果条件队列队尾节点取消排队了,将其移除队列,并向后遍历直到获取正常等待的节点
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //节点入队并成为新的队尾,初始化状态是 Node.CONDITION
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

/**
  * 遍历条件队列并将取消排队的节点踢出去 t.waitStatus != Node.CONDITION
  */
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

线程唤醒

while (!isOnSyncQueue(node)) {
//线程在这里挂起,等待节点被唤醒
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}

通过上面的代码我们知道当线程执行到LockSupport.park(this)时挂起等待被唤醒,为了便于理解,我们先看唤醒线程的方法。

/**
 * 唤醒操作其实就是将条件队列中的节点转移到阻塞队列
 */
public final void signal() {
    //执行signal的线程必须是持有当前独占锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

/**
 * 条件队列从前往后找到第一个需要被转移的节点,这里都是一些单向链表的操作
 */
private void doSignal(Node first) {
    do {
        //first节点要出队了,如果没有nextWaiter了,则将lastWaiter也设置为空
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        //出队
        first.nextWaiter = null;
    //这里循环,如果first转移不成功,则转移下一个,以此类推
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

/**
 * 这个方法中将节点转移到阻塞队列
 */
final boolean transferForSignal(Node node) {
    /*
     * 将waitStatus设置为0
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    /*
     * 将节点转移到阻塞队列队尾,返回的p是阻塞队列中的前驱节点
     */
    Node p = enq(node);
    int ws = p.waitStatus;

    /** 
     * ws > 0 说明前驱节点取消了排队,唤醒当前node对应的线程
     * ws <= 0 则将前驱节点状态设置为Node.SIGNAL,如果CAS失败则唤醒当前线程
     */
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

注意看ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)这个条件,正常情况下前驱节点在排队中ws=0,并且CAS成功状态变成Node.SIGNAL;那什么情况会唤醒线程呢?前驱节点取消排队或者CAS修改前驱节点状态失败,会唤醒线程。

检查中断状态

while (!isOnSyncQueue(node)) {
//线程在这里挂起,等待节点被唤醒
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}

被唤醒的线程从await方法中LockSupport.park处继续执行,checkInterruptWhileWaiting这个方法在线程被唤醒后就会执行,因为线程可能是被signal唤醒的,也可能是发生了中断被唤醒,所以while循环退出循环有两种情况:
1.线程发生了中断,要区分是在await期间中断还是signal后中断的,并将节点从条件队列转移到阻塞队列
2.没有发生中断,signal后等待节点被转移到阻塞队列,然后退出循环

/**
 * 检查中断状态,如果
 * 1.如果线程没有发生中断返回0
 * 2.线程发生中断并且是在await期间发生的,需要抛出异常 THROW_IE
 * 3.如果线程在signal之后发生中断,则需要重新设置中断状态 REINTERRUPT
 */
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
    (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
    0;
}

/**
* 只有中断状态才会调用此方法,判断节点是在await期间转移的还是signal后
*/
final boolean transferAfterCancelledWait(Node node) {
    //signal方法会将waitStatus设置为0
    //这个CAS成功说明waitStatus还是Node.CONDITION,说明中断是在await期间发生的
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        //即使被中断了,依然转移到阻塞队列
        enq(node);
        return true;
    }
    /*
     * signal之后发生了中断,自旋等待节点转移到阻塞队列
     */
    while (!isOnSyncQueue(node))
    Thread.yield();
    return false;
}

所以条件队列中线程即使发生了中断,不管是在await期间还是signal之后,节点都会转移到阻塞队列
条件队列节点向阻塞队列转移有两种情况:一是条件队列中的节点通过signal正常转移到阻塞队列;也可能是发生了中断,节点被唤醒后发现自己不是被signal的,也会主动进入阻塞队列中,不过中断状态interruptMode会被记录。PS:后面代码会对中断状态进行响应处理

获取独占锁

我们继续await方法的代码,在前面分析while循环结束后,节点已经进入阻塞队列,准备获取锁。

public final void await() throws InterruptedException {
    ...
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
	//acquireQueued 自旋获取锁
	//线程将阻塞获取锁,这个方法返回的时候,代表线程已经获取锁了
	//这个方法会返回线程是否被中断,如果线程在signal后中断,设置REINTERRUPT
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
	//signal中会有node.nextWaiter != null的处理
	//但如果await期间发生了中断,条件队列是没有signal代码的处理,节点没有出队,那么就在这里进行出队处理
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        //根据interruptMode处理中断状态
        reportInterruptAfterWait(interruptMode);
}

处理中断状态

对中断的两种情况进行处理:
THROW_IE:在await期间线程发生了中断,需要抛出异常,如果不在await期间响应中断,可以使用awaitUninterruptibly方法
REINTERRUPT:代表在await期间没有发生中断,而是在signal后线程发生了中断,重新设置中断状态。

private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

不处理中断

可以看到这个方法与await方法不同在不对中断进行响应处理了,条件队列中的线程即使发生了中断依然等待转移到阻塞队列,获取锁后只是重新设置了中断状态而已。

public final void awaitUninterruptibly() {
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    boolean interrupted = false;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        //这里没有break了,保存了一下interrupted
        if (Thread.interrupted())
            interrupted = true;
    }
    if (acquireQueued(node, savedState) || interrupted)
        //重新设置中断标识
        selfInterrupt();
}

响应中断

我们顺着中断的思路继续思考,假如线程在await期间发生了中断,会重新设置中断标识,之后节点还是会转移到阻塞队列等待获取独占锁,那么线程后面获取了锁后会对中断状态进行什么处理?答案就在acquireQueued方法中:acquireQueued方法返回的是boolean值,返回true表示线程是否发生了中断

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    //能执行到这里说明线程是被中断唤醒的
    selfInterrupt();
}

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

static void selfInterrupt() {
Thread.currentThread().interrupt();
}

线程从 LockSupport.park(this);唤醒后只是判断了一下中断状态,如果中断了设置interrupted=true,便进入了下一次for循环,直到拿到锁,将interrupted返回出去,所以只要线程等待锁期间发生过中断。interrupted就等于true。
acquire方法判断返回值为true,会执行selfInterrupt方法,设置了中断状态,至于怎么样响应中断,交给调用者来处理。也就是说,acquire方法本身是不响应中断的,举例线程A在等待获取锁,线程B对其进行了中断,线程A不会立即响应中断,继续等待锁,不过会记录了中断状态,以便调用者进行处理。
AQS中也提供了响应中断的方法acquireInterruptibly,还是上面的场景,不同的是线程A被中断后会取消抢锁,立即抛出InterruptedException 异常。

超时机制

ConditionObject中还有提供带超时机制的await方法,逻辑都差不多,我们选一个来分析

public final boolean await(long time, TimeUnit unit)
throws InterruptedException {
    long nanosTimeout = unit.toNanos(time);
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    //计算超时时间
    final long deadline = System.nanoTime() + nanosTimeout;
    boolean timedout = false;
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        //表示时间已到
        if (nanosTimeout <= 0L) {
            //将节点转移到阻塞队列去,如果转移成功,返回true表示是超时触发的
            //如果返回false表示调用过signal,节点已经被转移了,那就不存在超时了
            timedout = transferAfterCancelledWait(node);
            break;
        }
        //spinForTimeoutThreshold=1000L 小于一毫秒就不再parkNanos了
        if (nanosTimeout >= spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanosTimeout);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
        //超时时间与当前时间差值
        nanosTimeout = deadline - System.nanoTime();
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return !timedout;
}

带超时的await通过parkNanos来休眠指定时间,线程被唤醒后检查是否调用过signal,调用过没有超过,否则就是超时了,超时后节点转移到阻塞队列。

总结:

  1. Condition条件队列是一个单向链表结构,调用await方法的线程会被阻塞执行,包装成节点并加入链表尾部。
  2. 线程调用signal其实就是将条件队列节点向阻塞队列转移的过程,无论是否发生中断,节点都会转移到阻塞队列,然后阻塞等待获取锁。
  3. await方法是默认响应中断的,条件队列中的线程如果在await期间被中断,会设置中断状态,并在获取锁后抛出异常。而lock方法是不响应中断的,如果希望响应中断可以使用lockInterruptibly。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值