全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(三)条件变量

前两期我们已经掌握了AQS的基本结构、以及AQS是如何释放和获取资源的。其实到这里,我们已经掌握了AQS作为同步器的全部功能
不过,有些情况使用同步功能不够灵活,所以AQS又引入了操作系统中的另一个高度相关的概念——条件变量。由于条件变量的使用紧密依赖于AQS提供的释放、获取资源功能和同步队列,因此都放在了AQS源码中
能坚持看到这里的同学已经很不容易了,再接再厉,一起冲掉最后一座堡垒吧🦾🦾🦾

简介

条件变量是什么

条件对象这一概念源自于操作系统,设计它是为了解决等待同步需求,实现线程间协作通信的一种机制。Java其实也已经内置了条件变量,它和监视器锁是绑定在一起的,即Objectwaitnotify方法,使用这两个方法就可以实现线程之间的协作

Java中的条件变量直到Java 5才出现,用它来代替传统的Objectwaitnotify方法。相比waitnotifyConditionawaitsignal方法更加安全和高效,因为Condition是基于AQS实现的,加锁、释放锁的效率更高

条件变量顾名思义就是表示某种条件的变量。不过需要说明的是,条件变量中的条件并没有实际含义,仅仅只是一个标记,条件的含义需要代码来赋予

Condition接口

Condition是一个接口,条件变量都实现了Condition接口。该接口的基本方法是awaitsignalsignalAll这些

不过Condition依赖于Lock接口,需要借助Lock.newCondition方法来创建条件变量。因此Condition必然是和某个Lock绑定在一起的,这就和waitnotifyObject监视器锁绑定在一起一样。因此,Java中的条件变量必须配合锁使用

Condition和Java内置的条件变量方法之间的对应关系如下:

  • Condition.await 等价于 Object.wait
  • Condition.signal 等价于 Object.notify

不多BB了,直接看源码吧~

AQS之条件对象

AQS为条件变量的实现提供了95%以上的功能,Lock接口实现类一般只需要实现一下newCondition方法,以及AQS的isHeldExclusively方法,就可以直接使用条件变量了,你说方不方便!

AQS实现的方式就是直接提供了Condition的一个实现类——ConditionObject,我接下来就将其称为条件对象(可能不太严谨)。ConditionObject是AQS的内部类,可见性为public

条件对象的结构

每个ConditionObject都维护了一个条件队列,首尾节点分别由firstWaiterlastWaiter两个域来管理:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

每个在条件队列中等待的线程都由Node类来维护,它们之间通过Node类的nextWaiter相连,形成一个单向链表。每个NodewaitStatus都是Node.CONDITION,表明该线程正在某个条件变量的条件队列中等待ing~

条件对象的创建

十分的朴实无华且简单通透,要是所有代码都这么简单,那该有多好啊~

public ConditionObject() { }

正如前面所说,如果基于AQS实现的Lock接口实现类想要使用AQS提供的条件变量,只需要实现newCondition方法、isHeldExclusively方法即可。而实现newCondition方法一般只需要直接调用ConditionObject的构造方法即可,多么简单!

isHeldExclusively方法会在条件唤醒(signalsignalAll)中被调用,因为只有在持有互斥资源的情况下,才允许使用条件变量,在持有共享资源的情况下是不允许使用的!

接下来主要剖析一下ConditionObject是如何实现Condition接口的两大重要功能——条件等待条件唤醒

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15676704.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

条件等待

await

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;
    }
    // 能够离开while循环,说明被signal或被中断了,而且在同步队列中,所以可以调用acquireQueued
    // 而且之前释放了多少资源,现在就要还原回来,因此获取的参数是之前保存的state
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

addConditionWaiter方法会添加一个新节点到等待队列的队尾,该节点的waitStatusCONDITION。源码如下:

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 如果lastWaiter被取消(CANCELLED),就移除它以及前面所有被取消的节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);		// 为当前线程创建一个Node,waitStatus为CONDITION
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

unlinkCancelledWaiters会将所有被取消的Node从条件队列中移除,该方法只会在持有锁的情况下才会被调用,源码如下:

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 == nul)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

unlinkCancelledWaiters相当于是进行了一次正向遍历,将那些waitStatus不为CONDITION(即为CANCELLED)的Node给移除,挺简单的~

回到await方法,接下来调用fullyRelease方法保存当前的state,并调用一次release,最后返回保存的state。源码如下:

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;	// 如果失败就会将节点取消
    }
}

调用release就是为了完全释放当前线程所持有的资源,防止死锁。如果释放失败,说明当前线程压根没有获取到资源就调用了await方法,这是不允许的,会抛出IllegalMonitorStateException异常。这也解决了我之前的疑惑——如何保证调用unlinkCancelledWaiters之前必须持有锁呢? 这里就给出了答案!只有在当前线程持有资源的前提下,代码才能正常执行下去

回到await方法,接下来调用isOnSyncQueue方法来检查当前线程对应的Node同步队列上还是条件队列上,源码如下:

final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) // If has successor, it must be on queue
        return true;
    return findNodeFromTail(node);
}

该方法会先看目标nodewaitStatus是否为CONDITION,或者它的prev域是否为空,如果是,那么它一定在条件队列上,返回false;如果目标nodenext域不为空,那么它一定在同步队列上,返回true。如果以上两种快捷判断方法无法判定,那么需要调用findNodeFromTail进行暴力判定。总之,如果目标node在条件队列上等待,则返回true,否则说明在同步队列上,返回false

await中的while(!isOnSyncQueue)语义:如果当前节点所等待的条件没有满足,那么说明它还在条件队列上等待,就会一直被困在while循环中不停地被阻塞,不能离开while循环去竞争获取资源

findNodeFromTail方法会从tail开始向前遍历,以确定目标node是否位于同步队列上,很简单哦~其源码如下:

// 此方法只会被isOnSyncQueue方法调用
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

回到await方法,只有当node在条件队列上等待,才会执行while循环。在循环中,将当前线程(即node对应的线程)阻塞。如果被唤醒,就调用checkInterruptWhileWaiting方法检查是否在阻塞过程中被中断,如果发生了中断,该方法会返回非0值。while循环中如果发现被中断,则退出循环

checkInterruptWhileWaiting方法即其调用到的transferAfterCancelledWait方法,它们的源码如下:

/*
 * 不同返回值的含义:
 * 		0:没有发生中断
 * 		THROW_IE(-1):中断发生在被signal之前
 * 		REINTERRUPT(1):中断发生在被signal之后
 */
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
    0;
}

/*
 * transferAfterCancelledWait有两个作用:
 * 1、将目标node放到同步队列上
 * 2、检测node是否已经被signal,true表示还未被signal
final boolean transferAfterCancelledWait(Node node) {
    // 如果CAS成功,说明该node还未接收到signal信号
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    // 如果上面if中的CAS失败,说明node已经被signal
    // 那么就等待signal中的逻辑将node移动到同步队列上去,这里什么也不做,自旋等待即可
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

总之,如果线程在阻塞过程中没有发生中断,则checkInterruptWhileWaiting返回0,否则返回非0值
具体来说,如果发生中断,就会调用transferAfterCancelledWait方法将node转移到同步队列,并检测中断是发生在被signal之前还是之后。如果发生在被signal之前则返回THROW_IE(-1),如果发生在被signal之后则返回REINTERRUPT(1)

回到await方法的while循环中,如果没有被signal或被中断就会一直阻塞。如果被signal就不满足while条件,会退出循环;如果被中断而不是被signal,会直接break离开while循环
退出while循环说明此时已经位于同步队列(要么因为被signal而transfer到同步队列,要么因为被中断而调用transferAfterCancelledWait方法移动到同步队列),可以直接调用acquireQueued方法(调用该方法的前提:位于同步队列中)排队获取锁。获取虽然可能导致再次被阻塞,但这里是在同步队列上的阻塞,而不是在条件队列上的阻塞

退出处理是通过reportInterruptAfterWait方法,具体如何处理,取决于interruptMode的值。源码如下:

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

如果interruptModeTHROW_IE,那么说明该线程并未真正收到signal信号,只是因为被中断而被唤醒,所等待的条件并不一定被满足,于是抛出中断异常;如果是REINTERRUPT,调用selfInterrupt方法设置线程的中断状态

总的来说,await执行分为6步:
1、如果当前线程中断,那么抛出中断异常
2、为当前线程创建Node,并加入该条件变量的条件队列队尾
3、获取并保存下当前state,并使用保存的state来调用release方法,如果失败则抛出IllegalMonitorStateException异常
4、阻塞直到被signal或被中断
5、利用保存的state,调用acquireQueued方法重新获取状态
6、如果第四步期间被中断,则抛出中断异常

超时await

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) {
            timedout = transferAfterCancelledWait(node);
            break;
        }
        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`和无参数`await`基本差不多,执行一共分6步,不过最后一步会**返回是否超时**: 1、如果当前线程中断,那么抛出中断异常 2、为当前线程创建`Node`,并加入该条件变量的条件队列队尾 3、获取并保存下当前`state`,并使用保存的`state`来调用`release`方法,如果失败则抛出`IllegalMonitorStateException`异常 4、阻塞直到被`signal`或被中断,或**超时** 5、利用保存的`state`,调用`acquireQueued`方法重新获取状态 6、如果第四步期间被中断,则抛出中断异常;如果第四步是**因为超时而停止阻塞**,则返回false,否则返回true

awaitUninterruptibly

调用awaitUninterruptibly方法在等待过程中,不会因为中断而停止等待。源码如下:

public final void awaitUninterruptibly() {
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    boolean interrupted = false;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if (Thread.interrupted())
            interrupted = true;
    }
    if (acquireQueued(node, savedState) || interrupted)
        selfInterrupt();
}

`awaitUninterruptibly`执行分4步: 1、为当前线程创建`Node`,并加入该条件变量的条件队列队尾 2、获取并保存下当前`state`,并使用保存的`state`来调用`release`方法,如果失败则抛出`IllegalMonitorStateException`异常 3、阻塞直到被`signal` 4、利用保存的`state`,调用`acquireQueued`方法重新获取状态

awaitNanos

awaitNanos最多等待一定时间,可以被中断。其源码如下:

public final long awaitNanos(long nanosTimeout)
    throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    final long deadline = System.nanoTime() + nanosTimeout;
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        // 如果超时,则将node从条件队列转移到同步队列上去,并退出while循环
        if (nanosTimeout <= 0L) {
            transferAfterCancelledWait(node);
            break;
        }
        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 deadline - System.nanoTime();
}

`awaitNanos`执行分6步: 1、如果当前线程中断,那么抛出中断异常 2、为当前线程创建`Node`,并加入该条件变量的条件队列队尾 3、获取并保存下当前`state`,并使用保存的`state`来调用`release`方法,如果失败则抛出`IllegalMonitorStateException`异常 4、阻塞直到被`signal`或被中断或**超时** 5、利用保存的`state`,调用`acquireQueued`方法重新获取状态 6、如果第四步期间被中断,则抛出中断异常;最后**返回距离超时还剩多少纳秒**

awaitUntil

类似于awaitNanos,会返回是否是因为超时而终止等待。源码如下:

public final boolean awaitUntil(Date deadline)
    throws InterruptedException {
    long abstime = deadline.getTime();
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    boolean timedout = false;
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        if (System.currentTimeMillis() > abstime) {
            timedout = transferAfterCancelledWait(node);
            break;
        }
        LockSupport.parkUntil(this, abstime);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return !timedout;
}

`awaitUntil`和`awaitNanos`基本差不多,执行一共分6步,不过最后一步会返回是否超时: 1、如果当前线程中断,那么抛出中断异常 2、为当前线程创建`Node`,并加入该条件变量的条件队列队尾 3、获取并保存下当前`state`,并使用保存的`state`来调用`release`方法,如果失败则抛出`IllegalMonitorStateException`异常 4、阻塞直到被`signal`或被中断或超时 5、利用保存的`state`,调用`acquireQueued`方法重新获取状态 6、如果第四步期间被中断,则抛出中断异常;如果第四步是因为超时而停止阻塞,则返回false,否则返回true
作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15676704.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

条件唤醒

signal

signal用于唤醒某个等待在条件队列的线程。源码如下:

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

首先调用isHeldExclusively方法来判断当前线程是否持有互斥资源(isHeldExclusively方法只会在signalsignalAll中才会被调用),如果没有则抛出IllegalMonitorStateException异常。接下来调用doSignal方法将等待最久(队首)的线程从条件队列转移到同步队列

这里说明一下,只有当线程持有互斥资源时,才支持使用条件变量。关于这点,可以参考全网最详细的ReentrantReadWriteLock源码剖析(万字长文)
其中,写锁支持条件变量,而读锁不支持。因为写锁是互斥资源,而读锁是共享资源。原因也可以参见这篇博客里的解析,这里不再赘述

doSignal方法会从目标node(一般都是队首)开始,将遇到的第一个未被取消的线程从条件队列移除,并调用transferForSignal方法将其放到同步队列队尾去等待获取资源。源码如下:

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

transferForSignal方法不仅会判断目标node是否被取消,也会将目标node从条件队列移动到同步队列。源码如下:

final boolean transferForSignal(Node node) {
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))		// 如果CAS失败,说明node已经被取消了,直接返回false
        return false;

    Node p = enq(node);		// 入队,返回node的前驱节点p
    int ws = p.waitStatus;
    // 入队之后希望将前驱节点置为SIGNAL,表明node的线程正在苦等获取资源
    // 如果前驱节点已被取消,或CAS修改前驱的waitStatus失败,就干脆直接将node的线程唤醒,不多bb
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

总之,`signal`方法会将最早的(非取消)线程移动到同步队列去排队获取资源

signalAll

signalAll方法会将条件队列中所有的线程都移动到同步队列,让它们去获取资源。其源码如下:

public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}

该方法会调用doSignalAll方法将条件队列中所有的线程都移除,并放入同步队列。源码如下:

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;		// 将条件队列清空
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

总之,`signalAll`方法会将条件队列中所有的线程都移动到同步队列去排队获取资源
作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15676704.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

条件变量的应用

实现生产者-消费者模型

使用上面的非可重入锁NonReentrantLock的条件变量来实现简单的生产者-消费者模型(单生产者、单消费者),实现代码如下:

点击查看代码
public class ProducerConsumerModel {

    private static final NonReentrantLock lock = new NonReentrantLock();
    private static final Condition notFull = lock.newCondition();
    private static final Condition notEmpty = lock.newCondition();

    private static final Queue<String> queue = new LinkedList<>();
    private static final int queueLength = 10;        // 队列长度

    public static void main(String[] args) {
        Thread producer = new Thread(() -> {
            int i = 0;
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == queueLength) {
                        System.out.println("Queue is full!");
                        notFull.await();
                    }
                    Thread.sleep(2000);
                    System.out.println("Produce product: " + "product" + i);
                    queue.add("product" + i);
                    ++i;
                    notEmpty.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        });

        Thread consumer = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    while (queue.isEmpty()) {
                        System.out.println("Queue is empty!");
                        notEmpty.await();
                    }
                    Thread.sleep(2000);
                    String product = queue.poll();
                    System.out.println("Consume product: " + product);
                    notFull.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        });

        producer.start();
        consumer.start();
    }
}

好了,AQS系列共三期,到此为止,Bye~

全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础
全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(二)资源的获取和释放
全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(三)条件变量

posted @ 2021-12-22 01:05  酒冽  阅读(465)  评论(0编辑  收藏  举报