AQS(AbstractQueuedSynchronizer)源码深度解析(5)—条件队列的等待、通知的实现以及AQS的总结【一万字】

「这是我参与2022首次更文挑战的第29天,活动详情查看:2022首次更文挑战」。

详细介绍了AQS中的条件队列的等待、通知的实现以及AQS的总结。

AQS相关文章:

AQS(AbstractQueuedSynchronizer)源码深度解析(1)—AQS的设计与总体结构

AQS(AbstractQueuedSynchronizer)源码深度解析(2)—Lock接口以及自定义锁的实现

AQS(AbstractQueuedSynchronizer)源码深度解析(3)—同步队列以及独占式获取锁、释放锁的原理【一万字】

AQS(AbstractQueuedSynchronizer)源码深度解析(4)—共享式获取锁、释放锁的原理【一万字】

AQS(AbstractQueuedSynchronizer)源码深度解析(5)—条件队列的等待、通知的实现以及AQS的总结【一万字】

前面的文章讲解了AQS对于同步队列、各种独占锁、共享锁的实现原理,以及获取锁释放锁的方法。但是似乎还少了点什么,那就是Condition条件队列。

如果我们只有同步队列,确实可以实现线程同步,但是由于Java的线程实际上还是底层操作系统实现的,它具体分配多长的时间片、具体哪些线程需要等待、什么时候进入等待、哪些线程能够获得锁,都不是我们我们能够控制的,这样很难支持复杂的多线程需求。

而Condition条件队列则可用于实现主动的线程的等待、通知机制,是实现可控制的多线程编程中非常重要的一部分!

1 Condition概述

1.1 Object监视器与Condition

任意一个Java对象,都拥有一与之关联的唯一的监视器对象monitor(该对象是在HotSpot源码中使用C++实现的),为此Java为每个对象提供了一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。具体可以看Synchronized的原理:Java中的synchronized的底层实现原理以及锁升级优化详解

Condition(又称条件变量)接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

Object的监视器方法与Condition接口的对比如下(来自网络): 在这里插入图片描述

Condition可以和任意的锁对象结合,监视器方法不会再绑定到某个锁对象上。使用Lock锁之后,相当于Lock 替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。

在Condition中,Condition对象当中封装了监视器方法,并用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。如果在没有获取到锁前调用了条件变量的await 方法则会抛出java.lang.IllegalMonitorStateException 异常。

synchronized 同时只能与一个共享变量的notify 或wait 方法实现同步,而AQS 的一个锁可以对应多个条件变量。

Condition的强大之处还在于它可以为多个线程间建立不同的Condition,使用synchronized/wait()只有一个条件队列,notifyAll会唤起条件队列下的所有线程,而使用lock-condition,可以实现多个条件队列,signalAll只会唤起某个条件队列下的等待线程。

另外AQS只提供了ConditionObject的实现,并没有提供获取Condition的newCondition方法对应的模版方法,需要由AQS的子类来提供具体实现,通常是直接调用ConditionObject的构造器new一个对象返回。一个锁对象可以多次调用newCondition方法,因此一个锁对象可以对应多个Condition对象!

1.2 常用API方和使用示例

方法名称 描述
void await() throws InterruptedException 当前线程进入等待状态直到被通知或中断。该方法返回时,该线程肯定又一次获取了锁。
void awaitUninterruptibly() 当前线程进入等待状态直到被通知,等待过程中不响应中断。该方法返回时,该线程肯定又一次获取了锁。
long awaitNanos(long nanosTimeout) throws InterruptedException 当前线程进入等待状态直到被通知,中断,或者超时。返回(超时时间 - 实际返回所用时间)。如果返回值是0或者负数,那么可以认定已经超时了。该方法返回时,该线程肯定又一次获取了锁。
boolean await(long time, TimeUnit unit) throws InterruptedException 当前线程进入等待状态直到被通知,中断,或者超时。如果在从此方法返回前检测到等待时间超时,则返回 false,否则返回 true。该方法返回时,该线程肯定又一次获取了锁。
boolean awaitUntil(Date deadline) throws InterruptedException 当前线程进入等待状态直到被通知,中断或者超过指定时间点。如果没有到指定时间就被通知,则返回true,否则返回false。
void signal() 唤醒一个在Condition上等待最久的线程,该线程从等待方法返回前必须获得与Condition相关联的锁
void signalAll() 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。

下面示例Condition实现有界同步队列(生产消费):

首先需要获得锁,目的是确保数组修改的可见性和排他性。当数组数量等于数组长度时,表示数组已满,则调用notFull.await(),当前线程随之释放锁并进入等待状态。如果数组数量不等于数组长度,表示数组未满,则添加元素到数组中,同时通知等待在notEmpty上的线程,数组中已经有新元素可以获取。

在添加和删除方法中使用while循环而非if判断,目的是防止过早或意外的通知,只有条件符合才能够退出循环。

/**
 * 使用Condition实现有界队列
 */
public class BoundedQueue<T> {
    //数组队列
    private Object[] items;
    //添加下标
    private int addIndex;
    //删除下标
    private int removeIndex;
    //当前队列数据数量
    private int count;
    //互斥锁
    private Lock lock = new ReentrantLock();
    //队列不为空的条件
    private Condition notEmpty = lock.newCondition();
    //队列没有满的条件
    private Condition notFull = lock.newCondition();

    public BoundedQueue(int size) {
        items = new Object[size];
    }

    //添加一个元素,如果数组满了,添加线程进入等待状态,直到有“空位”
    public void add(T t) {
        lock.lock();
        try {
            while (count == items.length) {
                notFull.await();
            }
            items[addIndex] = t;
            if (++addIndex == items.length) {
                addIndex = 0;
            }
            ++count;
            //唤醒一个等待删除的线程
            notEmpty.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //由头部删除一个元素,如果数组空,则删除线程进入等待状态,知道有新元素加入
    public T remove() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();
            }
            Object res = items[removeIndex];
            if (++removeIndex == items.length)
                removeIndex = 0;
            --count;
            //唤醒一个等待插入的线程
            notFull.signal();
            return (T) res;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public String toString() {
        return "BoundedQueue{" +
                "items=" + Arrays.toString(items) +
                ", addIndex=" + addIndex +
                ", removeIndex=" + removeIndex +
                ", count=" + count +
                ", lock=" + lock +
                ", notEmpty=" + notEmpty +
                ", notFull=" + notFull +
                '}';
    }

    public static void main(String[] args) throws InterruptedException {
        BoundedQueue<Object> objectBoundedQueue = new BoundedQueue<>(10);
        for (int i = 0; i < 20; i++) {
            objectBoundedQueue.add(i);
            System.out.println(objectBoundedQueue);
            if (i/2==0) {
                objectBoundedQueue.remove();
            }
        }
    }
}
复制代码

2 条件队列的结构

每一个AQS对象中包含一个同步队列,类似的,每个Condition对象中都包含着一个队列(以下称为等待/条件队列),用来存放调用该Condition对象的await()方法时被阻塞的线程。该队列是Condition实现等待/通知机制的底层关键数据结构。

条件队列同样是一个FIFO的队列,结点的类型直接复用的同步队列的结点类型—AQS的静态内部类AbstractQueuedSynchronizer.Node。一个Condition对象的队列中每个结点包含的线程就是在该Condition对象上等待的线程,那么如果一个锁对象获取了多个Condition对象,就可能会有不同的线程在不同的Condition对象上等待!

如果一个获取到锁的线程调用了Condition.await()方法,那么该线程将会被构造成等待类型为Node.CONDITION的Node结点加入等待队列尾部并释放锁,加入对应Condition对象的条件队列尾部并挂起(WAITING)。

如果某个线程中调用某个Condition的signal/signalAll方法,对应Condition对象的条件队列的结点会转移到锁内部的AQS对象的同步队列中,并且在获取到锁之后,对应的线程才可以继续恢复执行后续代码。

ConditionObject中持有条件队列的头结点引用firstWaiter和尾结点引用lastWaiter。

public abstract class AbstractQueuedSynchronizer
            extends AbstractOwnableSynchronizer
            implements java.io.Serializable {
    /**
     * 同步队列头节点
     */
    private transient volatile Node head;

    /**
     * 同步队列尾节点
     */
    private transient volatile Node tail;

    /**
     * 同步状态
     */
    private volatile int state;
    
    /**
     * Node节点的实现
     */
    static final class Node {
        //……
    }
    
    /**
     * 位于AQS内部的ConditionObject类,就是Condition的实现
     */
    public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /**
         * 条件队列头结点引用
         */
        private transient Node firstWaiter;
        /**
         * 条件队列尾结点引用
         */
        private transient Node lastWaiter;
        
        //……
    }
}
复制代码

Condition的实现是AQS的内部类ConditionObject,因此每个Condition实例都能够访问AQS提供的方法,相当于每个Condition都拥有所属AQS的引用。

和AQS中的同步队列不同的是,条件队列是一个单链表,结点之间使用nextWaiter引用维持后继的关系,并不会用到prev, next属性,它们的值都为null,并且没有哨兵结点,大概结构如下:

在这里插入图片描述

如图所示,Condition拥有首尾结点的引用,而新增结点只需要将原有的尾结点nextWaiter指向它,并且更新尾结点即可。上述结点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

在Object的监视器模型上,一个监视器对象只能拥有一个同步队列和等待队列,而JUC中的一个同步组件实例可以拥有一个同步队列和多个条件队列,其对应关系如下图:

在这里插入图片描述

3 等待机制原理

调用Condition的await()、await(long time, TimeUnit unit)、awaitNanos(long nanosTimeout)、awaitUninterruptibly()、awaitUntil(Date deadline)方法时,会使当前线程构造成一个等待类型为Node.CONDITION的Node结点加入等待队列尾部并释放锁,同时线程状态变为等待状态(WAITING)。而当从await()方法返回时,当前线程一定获取了Condition相关联的锁。

对于同步队列和条件队列这两个队列来说,当调用await()方法时,相当于同步队列的头结点(获取了锁的结点)移动到Condition的等待队列成为尾结点(只是一个比喻,实际上并没有移动)。

AQS的Node的waitStatus使用Node.CONDITION(-2)来表示结点处于等待状态,如果条件队列中的结点不是Node.CONDITION状态,那么就认为该结点就不再等待了,需要出队列。

3.1 await()响应中断等待

调用该方法的线程也一定是成功获取了锁的线程,也就是同步队列中的首结点,如果一个没有获得锁的线程调用此方法,那么可能会抛出异常!

await方法用于将当前线程构造成结点并加入等待队列中,并释放锁,然后当前线程会进入等待状态,等待唤醒。

当等待队列中的结点线程因为signal、signalAll或者被中断而唤醒,则会被移动到同步队列中,然后在await中尝试获取锁。如果是在其他线程调用signal、signalAll方法之前就因为中断而被唤醒了,则会抛出InterruptedException,并清除当前线程的中断状态。

该方法响应中断。最终,如果该方法能够返回,那么该线程一定是又一次重新获取到锁了。

大概步骤为:

  1. 最开始就检查一次,如果当前线程是被中断状态,则清除已中断状态,并抛出异常
  2. 调用addConditionWaiter方法,将当前线程封装成Node.CONDITION类型的Node结点链接到条件队列尾部,返回新加的结点,该过程中将移除取消等待的结点。
  3. 调用fullyRelease方法,一次性释放当前线程所占用的所有的锁(重入锁),并返回取消时的同步状态state 值。
  4. 循环,调用isOnSyncQueue方法判断结点是否被转移到了同步队列中:
    1. 如果不在同步队列中,那么park挂起当前线程,不在执行后续代码。
    2. 如果被唤醒,那么调用checkInterruptWhileWaiting检查线程被唤醒的原因,并且使用interruptMode字段记录中断模式。
    3. 如果此时线程时中断状态,那么break跳出循环,否则,进行下一次循环判断。
  5. 到这一步,结点一定是加入同步队列中了。那么使用acquireQueued自旋获取独占锁,将锁重入次数原封不动的写回去。
  6. 获取到锁之后,判断如果在获取锁的等待过程中被中断,并且之前的中断模式不为THROW_IE(可能是0),那么设置中断模式为REINTERRUPT。
  7. 如果结点后继不为null,说明是“在调用signal或者signalAll方法之前就因为中断而被唤醒”的情况,发生这种情况时结点是没有从条件队列中移除的,此时需要移除。这里直接调用调用unlinkCancelledWaiters对条件队列再次进行整体清理。
  8. 如果中断模式不为0,那么调用reportInterruptAfterWait方法对不同的中断模式做出处理。
/**
 * 位于ConditionObject中的方法
 * 当前线程进入等待状态,直到被通知或中断
 *
 * @throws InterruptedException 如果线程被中断,那么返回并抛出异常
 */
public final void await() throws InterruptedException {
    /*最开始就检查一次,如果当前线程是被中断状态,则清除已中断状态,并抛出异常*/
    if (Thread.interrupted())
        throw new InterruptedException();
    /*当前线程封装成Node.CONDITION类型的Node结点链接到条件队列尾部,返回新加的结点*/
    Node node = addConditionWaiter();
    /*尝试释放当前线程所占用的所有的锁,并保存当前的锁状态*/
    int savedState = fullyRelease(node);
    //中断模式,默认为0 表示没有中断,后面会介绍
    int interruptMode = 0;
/*循环检测,如果当前队列不在同步队列中,那么将当前线程继续挂起,停止执行后续代码,直到被通知/中断;
否则,表示已在同步队列中,直接跳出循环*/
    while (!isOnSyncQueue(node)) {
        //此处线程阻塞
        LockSupport.park(this);
        // 走到这一步说明可能是被其他线程通知唤醒了或者是因为线程中断而被唤醒
        // checkInterruptWhileWaiting检查线程被唤醒的原因,并且使用interruptMode字段记录中断模式
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            //如果是中断状态,则跳出循环,这说明中断状态也会离开条件队列加入同步队列
            break;
        /*如果没有中断,那就是因为signal或者signalAll方法的调用而被唤醒的,并且已经被加入到了同步队列中
         * 在下一次循环时,将不满足循环条件,而自动退出循环*/
    }
    /*
     * 到这一步,结点一定是加入同步队列中了
     * 那么使用acquireQueued自旋获取独占锁,第二个参数就是最开始释放锁时的同步状态,这里要将锁重入次数原封不动的写回去
     * 如果在获取锁的等待过程中被中断,并且之前的中断模式不为THROW_IE(可能是0),那么设置中断模式为REINTERRUPT,
     * 即表示在调用signal或者signalAll方法之后设置的中断状态
     * */
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    /*此时已经获取到了锁,那么实际上这个对应的结点就是head结点了
     *但是如果线程是 在调用signal或者signalAll方法之前就因为中断而被唤醒 的情况时,将结点添加到同步队列的的时候,并没有清除在条件队列中的结点引用
     *因此,判断nextWaiter是否不为null,如果是则还需要从条件队列中移除彻底移除这个结点。
     * */
    if (node.nextWaiter != null)
        //这里直接调用unlinkCancelledWaiters方法移除所有waitStatus不为CONDITION的结点
        unlinkCancelledWaiters();
    //如果中断模式不为0,那么调用reportInterruptAfterWait方法对不同的中断模式做出处理
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
复制代码

3.1.1 addConditionWaiter添加结点到条件队列

同步队列的首结点并不会直接加入等待队列,而是通过addConditionWaite方法把当前线程构造成一个新的结点并将其加入等待队列中。

addConditionWaiter方法用于将当前线程封装成Node.CONDITION类型的结点链接到条件队列尾部。大概有两步:

  1. 首先获取条件队列尾结点,如果尾结点不是等待状态,那么调用unlinkCancelledWaiters对整个链表做一个清理,清除不是等待状态的结点;
  2. 将当前线程包装成Node.CONDITION类型的Node加入条件队列尾部,这里不需要CAS,因为此时线程已经获得了锁,不存在并发的情况。
/**
 * 位于ConditionObject中的方法
 * 当前线程封装成Node.CONDITION类型的Node结点链接到条件队列尾部
 *
 * @return 新添加的结点
 */
private Node addConditionWaiter() {
    //获取条件队列尾结点t
    Node t = lastWaiter;
    /*1 如果t的状态不为Node.CONDITION,即不是等待状态了遍历整个条件队列链表,清除所有不在等待状态的结点*/
    if (t != null && t.waitStatus != Node.CONDITION) {
        /*遍历整个条件队列链表,清除所有不在等待状态的结点*/
        unlinkCancelledWaiters();
        //获取最新的尾结点
        t = lastWaiter;
    }
    /*2 将当前线程包装成Node.CONDITION类型的Node加入条件队列尾部
     * 这里不需要CAS,因为此时线程已经获得了锁,不存在并发的情况
     * 从这里也能看出来,条件队列仅仅是一条通过nextWaiter维持后继关系的单链表,同时不存在类似于同步队列的哨兵结点
     * */
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    //返回新加结点
    return node;
}
复制代码

3.1.2 unlinkCancelledWaiters清除取消等待的结点

unlinkCancelledWaiters方法会从头开始遍历整个单链表,清除所有取消等待的结点,比较简单!

/**
 * 位于ConditionObject中的方法
 * 从头开始遍历整个条件队列链表,清除所有不在等待状态的结点
 */
private void unlinkCancelledWaiters() {
    //t用来记录当前结点的引用,从头结点开始
    Node t = firstWaiter;
    //trail用来记录上一个非取消(Node.CONDITION)结点的引用
    Node trail = null;
    /*头结点不为null,则遍历链表*/
    while (t != null) {
        //获取后继
        Node next = t.nextWaiter;
        /*如果当前结点状态不是等待状态(Node.CONDITION),即取消等待了*/
        if (t.waitStatus != Node.CONDITION) {
            //当前结点的后继引用置空
            t.nextWaiter = null;
            //trail为null,出现这种情况,只能是第一次循环时,就发现头结点取消等待了
            // 则头结点指向next
            if (trail == null)
                firstWaiter = next;
            //否则trail的后继指向next
            else
                trail.nextWaiter = next;
            //如果next为null,说明到了尾部,则lastWaiter指向上一个非取消(Node.CONDITION)结点的引用,该结点就是尾结点
            if (next == null)
                lastWaiter = trail;
        }
        /*否则,trail指向t*/
        else
            trail = t;
        //t指向next,相当于遍历链表了
        t = next;
    }
}
复制代码

3.1.3 fullyRelease释放所有重入锁

await中锁的释放都是独占式的。由于可能是可重入锁,因此fullyRelease方法会将当前获取锁的线程的全部重入锁都一次性释放掉。例如某个线程的锁重入了一次,此时state变成2,在await中会一次性将2变成0。

我们常说说await方法必须要在获取锁之后调用,因为在fullyRelease中会调用release独占式的释放锁,而release中调用了tryRelease方法,对于独占锁的释放,我们的实现会检查是否是当前获取锁的线程,如果不是,那么会抛出IllegalMonitorStateException异常。

fullyRelease大概步骤如下:

  1. 获取当前的同步状态state的值savedState,调用release释放全部锁包括重入的;
  2. 释放成功那么返回savedState,如果不是当前获取锁的线程那么会抛出异常。
  3. 释放失败同样也会抛出IllegalMonitorStateException异常。
  4. finally中,释放成功什么也不做;释放失败则将新添加进条件队列的结点状态设置为Node.CANCELLED,即算一种非等待状态。
/**
 * 位于AQS中的方法,在结点被添加到条件队列中之后调用
 * 尝试释放当前线程所占用的所有的锁,并返回当前的锁的同步状态state
 *
 * @param node 刚添加的结点
 * @return 当前的同步状态
 */
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        //获取此时state值,
        int savedState = getState();
        //因为state可能表锁重入次数
        //这里调用release释放锁,参数为此时state的值,表示释放全部锁,就算是重入锁也一次性释放完毕
        //我们常说说await方法必须要在获取锁之后调用,就是这个方法的逻辑实现的:
        //这个方法中调用了tryRelease方法,对于独占锁我们的实现会检查是否是当前获取锁的线程,如果不是,那么会抛出IllegalMonitorStateException异常
        //release释放之后还会唤醒同步队列的一个非取消的结点
        if (release(savedState)) {
            //释放完成之后
            failed = false;
            //返回释放之前的state同步状态
            return savedState;
        } 
        /*如果释放失败,那么也直接抛出异常*/
        else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        //如果释放失败,可能是抛出异常,那么新加入结点的状态改成Node.CANCELLED,即算一种非等待状态
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

复制代码

3.1.4 isOnSyncQueue结点是否在同步队列中

isOnSyncQueue用于检测结点是否在同步队列中,如果await等待的线程被唤醒/中断,那么对应的结点会被转移到同步队列之中!

/**
 * 位于AQS中的方法,在fullyRelease之后调用
 * 判断结点是否在同步队列之中
 *
 * @param node 新添加的结点
 * @return 如果在返回true,否则返回false
 */
final boolean isOnSyncQueue(Node node) {
    //如果状态为Node.CONDITION那一定是不在同步队列中
    //或者如果前驱为null,表示肯定不在同步队列中,因为加入队列的第一步就是设置前驱
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    //如果next不为null,说明肯定在同步队列中了,而且还不是在尾部
    if (node.next != null)
        return true;
    /*
    有可能状态不为Node.CONDITION并且node.prev的值不为null,此时还没彻底添加到队列中,但是不知道后续会不会添加成功,因为enq入队时CAS变更的tail可能失败。
    此时需要调用findNodeFromTail从后向前遍历整个同步队列,查找是否有该结点
     */
    return findNodeFromTail(node);
}

/**
 * 从尾部开始向前遍历同步队列,查找是否存在指定结点
 *
 * @param node 指定结点
 * @return 如果存在,返回true
 */
private boolean findNodeFromTail(Node node) {
    //遍历同步队列,查找是否存在该结点
    Node t = tail;
    for (; ; ) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}
复制代码

3.1.5 checkInterruptWhileWaiting检测中断以及被唤醒原因

await将线程中断的状态根据在不同情况下的中断分成三种模式,除了下面的两种之外,还使用0表示没有中断。在await方法的最后,会根据不同的中断模式做出不同的处理。

/**
 * 中断模式
 * 退出await方法之前,需要设置中断状态,由于此时已经获得了锁,即相当于设置一个标志位。
 * 如果是在调用signal或者signalAll方法之后被中断,会是这个模式
 */
private static final int REINTERRUPT = 1;
/**
 * 中断模式
 * 退出await方法之前,需要抛出InterruptedException异常
 *如果是在等待调用signal或者signalAll之前就被中断了,会是这个模式
 */
private static final int THROW_IE = -1;
复制代码

checkInterruptWhileWaiting方法在挂起的线程被唤醒之后调用,用于检测被唤醒的原因,并返回中断模式。大概步骤为:

  1. 判断此时线程的中断状态,并清除中断状态。如果是中断状态,那么调用transferAfterCancelledWait方法判断那是在什么时候中断的;否则,返回0,说明是调用signal或者signalAll方法之后被唤醒的,并且没有中断。
  2. transferAfterCancelledWait方法中,如果是因为在调用signal或者signalAll之前就被中断了,那么会将该结点状态设置为0,并调用enq方法加入到同步队列中,返回THROW_IE模式,表示在await方法的最后会抛出异常;否则那就是在调用signal或者signalAll方法之后被中断的,那么在等待结点成功加入同步队列之后,返回REINTERRUPT模式,表示在await方法最后会重新设置中断状态。
/**
 * Condition中的方法
 * 检查被唤醒线程的中断状态,返回中断模式
 */
private int checkInterruptWhileWaiting(Node node) {
    //检查中断状态,并清除中断状态
    return Thread.interrupted() ?
            //如果是中断状态,那么调用transferAfterCancelledWait方法判断是在什么时候被中断的
            //如果在调用signal或者signalAll之前被中断,那么返回THROW_IE,表示在await方法最后会抛出异常
            // 否则返回REINTERRUPT,表示在await方法最后会重新设置中断状态
            (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
            //如果是未中断状态,那么返回0
            0;
}

/**
 * AQS中的方法
 * 判断是在什么时候被中断的
 *
 * @param node 指定结点
 * @return 如果在signal或者signalAll之前被中断,将返回true;否则返回false
 */
final boolean transferAfterCancelledWait(Node node) {
    //我们需要知道:signal或者signalAll方法中会将结点状态从Node.CONDITION设置为0
    //这里再试一次,如果在这里将指定node从Node.CONDITIONCAS设置为0成功,那么表示该结点肯定是在signal或者signalAll方法被调用之前 被中断而唤醒的
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        /*即使是因为这样被唤醒,此时也需要手动将结点加入同步队列尾部
        注意此时并没有将结点移除条件队列,因此在await方法的最后,会有这样的判断:
         if (node.nextWaiter != null) // clean up if cancelled
         unlinkCancelledWaiters();
         实际上就是再判断这种情况!
        */
        enq(node);
        //入队成功,返回true
        return true;
    }
    /*否则,表示是在signal或者signalAll被调用之后,又被设置了中断状态的
     * signal或者signalAll的方法中会将结点添加到同步队列中,这里循环判断到底在不在队列中,
     * 因为或者signal或者signalAll方法可能还没有执行完毕,这里等它执行完毕,然后返回false
     * 如果不等他执行完毕,在回到外面的await方法中时,可能会影响后续的重新获取锁acquireQueued方法的执行
     * */
    while (!isOnSyncQueue(node))
        Thread.yield();
    //确定被加入到了同步队列,则返回false
    return false;
}
复制代码

3.1.6 reportInterruptAfterWait对中断模式进行处理

在await方法的最后,如果中断模式不为0,那么会对之前设置的中断模式进行统一处理:

  1. 如果是THROW_IE模式,即在调用signal或者signalAll之前就被中断了,那么抛出InterruptedException异常。
  2. 如果是REINTERRUPT模式,即在调用signal或者signalAll方法之后被中断,那么简单的设置一个中断状态为true。
/**
 * 中断模式的处理:
 * 如果是THROW_IE模式,那么抛出InterruptedException异常
 * 如果是REINTERRUPT模式,那么简单的设置一个中断状态结尾true
 */
private void reportInterruptAfterWait(int interruptMode)
        throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}
复制代码

3.2 await(time, TimeUnit)超时等待一段时间

当前线程进入等待状态直到被通知、中断,或者超时。

如果在超时时间范围之内被唤醒了,则返回true;否则返回false。

该方法响应中断。最终,如果该方法能够返回,那么该线程一定是又一次重新获取到锁了。

/**
 * 超时等待
 *
 * @param time 时长
 * @param unit 时长单位
 * @return 如果在超时时间范围之内被唤醒了,则返回true;否则返回false
 * @throws InterruptedException 如果一开始就是中断状态或者如果在signal()或signalALL()方法调用之前就因为中断而被唤醒,那么抛出异常
 */
public final boolean await(long time, TimeUnit unit)
        throws InterruptedException {
    /*内部结构和await方法差不多*/
    //将超时时间转换为纳秒
    long nanosTimeout = unit.toNanos(time);
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    //获取最大超时时间对应的时钟纳秒值
    final long deadline = System.nanoTime() + nanosTimeout;
    //超时标志位timedout
    boolean timedout = false;
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        //超时时间如果小于等于0,表示已经超时了,还没有返回,此时主动将结点加入同步队列。
        if (nanosTimeout <= 0L) {
            //直接调用transferAfterCancelledWait,这里的transferAfterCancelledWait方法被用来将没有加入队列的结点直接加入队列,或者等待结点完成入队
            //该方法中,如果检测到该结点此时(已超时)还没被加入同步队列,则手动添加并返回true;否则,等待入队完成并返回false
            timedout = transferAfterCancelledWait(node);
            //结束循环
            break;
        }
        //如果超时时间大于1000,则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);
    //注意:最终返回的是!timedout
    // 即如果在parkNanos时间范围之内被唤醒了,则返回true,否则如果是自然唤醒的,则返回false
    return !timedout;
}
复制代码

3.3 awaitUntil(deadline)超时等待时间点

与await(time, TimeUnit)的行为一致,不同之处在于该方法的参数不是一段时间,而是一个时间点。

如果在超时时间点之前被唤醒了,则返回true;否则返回false。

该方法响应中断。最终,如果该方法能够返回,那么该线程一定是又一次重新获取到锁了。

/**
 * 超时等待指定时间点
 *
 * @param deadline 超时时间点
 * @return 如果在超时时间点之前被唤醒了,则返回true;否则返回false
 * @throws InterruptedException 如果一开始就是中断状态或者如果在signal()或signalALL()方法调用之前就因为中断而被唤醒,那么抛出异常
 */
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);
    //超时标志位timedout
    boolean timedout = false;
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        //如果当前时间毫秒 大于 指定时间点的毫秒,表明已经超时了,还没有返回,此时主动将结点加入同步队列。
        if (System.currentTimeMillis() > abstime) {
            //直接调用transferAfterCancelledWait,这里的transferAfterCancelledWait方法被用来将没有加入队列的结点直接加入队列,或者等待结点完成入队
            //该方法中,如果检测到该结点此时(已超时)还没被加入同步队列,则手动添加并返回true;否则,等待入队完成并返回false
            timedout = transferAfterCancelledWait(node);
            break;
        }
        //parkUntil方法使线程睡眠到指定时间点
        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);
    //注意:最终返回的是!timedout
    // 即如果在指定时间点之前被唤醒了,则返回true,否则返回false
    return !timedout;
}
复制代码

3.4 awaitNanos(nanosTimeout) 超时等待纳秒

当前线程进入等待状态直到被通知,中断,或者超时。

返回(超时时间 - 实际返回所用时间)。如果返回值是0或者负数,那么可以认定已经超时了。

该方法响应中断。最终,如果该方法能够返回,那么该线程一定是又一次重新获取到锁了。

/**
 * 超时等待指定纳秒,若指定时间内返回,则返回 nanosTimeout-已经等待的时间;
 *
 * @param nanosTimeout 超时时间,纳秒
 * @return 返回 超时时间 - 实际返回所用时间
 * @throws InterruptedException 如果一开始就是中断状态或者如果在signal()或signalALL()方法调用之前就因为中断而被唤醒,那么抛出异常
 */
public final long awaitNanos(long nanosTimeout)
        throws InterruptedException {
    /*内部结构和await方法差不多*/
    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)) {
        //超时时间如果小于等于0,表示已经超时了,还没有返回,此时主动将结点加入同步队列。
        if (nanosTimeout <= 0L) {
            //直接调用transferAfterCancelledWait,这里的transferAfterCancelledWait方法被用来将没有加入队列的结点直接加入队列,或者等待结点完成入队
            //这里不需要返回值
            transferAfterCancelledWait(node);
            break;
        }
        //如果超时时间大于1000,则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);
    //返回 最大超时时间对应的时钟纳秒值 减去 当前时间的纳秒值
    //可以想象,如果在指定时间之内返回,那么将是正数,否则是0或负数
    return deadline - System.nanoTime();
}
复制代码

3.5 awaitUninterruptibly()不响应中断等待

当前线程进入等待状态直到被通知,不响应中断。

其它线程调用该条件对象的signal()或signalALL()方法唤醒等待的线程之后,该线程才可能从awaitUninterruptibly方法中返回。等待过程中如果当前线程被中断,该方法仍然会继续等待,同时保留该线程的中断状态。

最终,如果该方法能够返回,那么该线程一定是又一次重新获取到锁了。

/**
 * 当前线程进入等待状态直到被通知(signal或者signalAll)而唤醒,不响应中断。
 */
public final void awaitUninterruptibly() {
    /*内部结构和await方法差不多,比await更加简单,因为不需要响应中断*/
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    boolean interrupted = false;
    /*循环,只有其他线程调用对应Condition的signal或者signalAll方法,将该结点加入到同步队列中时,才会停止循环*/
    while (!isOnSyncQueue(node)) {
        //直接等待
        LockSupport.park(this);
        //被唤醒之后,会判断如果被中断了,那么清除当前线程的中断状态。记录中断标志位,然后继续循环
        //在下一次的循环条件中会判断是否被加入了同步队列
        if (Thread.interrupted())
            interrupted = true;
    }
    //如果获取锁的等待途中被中断过,或者前面的中断标志位为true
    if (acquireQueued(node, savedState) || interrupted)
        //安全中断,实际上就是将当前线程的中断标志位改为true,记录一下而已
        selfInterrupt();
}
复制代码

4 通知机制原理

上面讲了等待机制,并且提起了signal和signalAll方法将会唤醒等待的线程,这就是Condition的通知机制,相比于等待机制,通知机制还是比较简单的!

4.1 signal通知单个线程

signal方法首先进行了isHeldExclusively检查,也就是当前线程必须是获取了锁的线程,否则抛出异常。接着将会尝试唤醒在条件队列中等待时间最长的结点,将其移动到同步队列并使用LockSupport唤醒结点中的线程。

大概步骤如下:

  1. 检查调用signal方法的线程是否是持有锁的线程,如果不是则直接抛出IllegalMonitorStateException异常。
  2. 调用doSignal方法将等待时间最长的一个结点从条件队列转移至同步队列尾部,然后根据条件可能会尝试唤醒该结点对应的线程。
/**
 * Conditon中的方法
 * 将等待时间最长的结点移动到同步队列,然后unpark唤醒
 *
 * @throws IllegalMonitorStateException 如果当前调用线程不是获取锁的线程,则抛出异常
 */
public final void signal() {
    /*1 首先调用isHeldExclusively检查当前调用线程是否是持有锁的线程
     * isHeldExclusively方法需要我们重写
     * */
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //获取头结点
    Node first = firstWaiter;
    /*2 如果不为null,调用doSignal方法将等待时间最长的一个结点从条件队列转移至同步队列尾部,然后根据条件可能会尝试唤醒该结点对应的线程。*/
    if (first != null)
        doSignal(first);
}


/**
 * AQS中的方法
 * 检测当前线程是否是持有独占锁的线程,该方法AQS没有提供实现(抛出UnsupportedOperationException异常)
 * 通常需要我们自己重写,一般重写如下!
 *
 * @return true 是;false 否
 */
protected final boolean isHeldExclusively() {
    //比较获取锁的线程和当前线程
    return getExclusiveOwnerThread() == Thread.currentThread();
}
复制代码

4.1.1 doSignal移除-转移等待时间最长的结点

doSignal方法将在do while中从头结点开始向后遍历整个条件队列,从条件队列中移除等待时间最长的结点,并将其加入到同步队列,在此期间会清理一些遍历时遇到的已经取消等待的结点。

/**
 * Conditon中的方法
 * 从头结点开始向后遍历,从条件队列中移除等待时间最长的结点,并将其加入到同步队列
 * 在此期间会清理一些遍历时遇到的已经取消等待的结点。
 *
 * @param first 条件队列头结点
 */
private void doSignal(Node first) {
    /*从头结点开始向后遍历,唤醒等待时间最长的结点,并清理一些已经取消等待的结点*/
    do {
        //firstWaiter指向first的后继结点,并且如果为null,则lastWaiter也置为null,表示条件队列没有了结点
        if ((firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        //first的后继引用置空,这样就将first出队列了
        first.nextWaiter = null;
        /*循环条件
         * 1 调用transferForSignal转移结点,如果转移失败(结点已经取消等待了);
         * 2 则将first赋值为它的后继,并且如果不为null;
         * 满足上面两个条件,则继续循环
         * */
    } while (!transferForSignal(first) &&
            (first = firstWaiter) != null);
}
复制代码

4.1.1.1 transferForSignal转移结点

transferForSignal会尝试将遍历到的结点转移至同步队列中,调用该方法之前并没有显示的判断结点是不是处于等待状态,而是在该方法中通过CAS的结果来判断。

大概步骤为:

  1. 尝试CAS将结点等待状态从Node.CONDITION更新为0。这里不存在并发的情况,因为调用线程此时已经获取了独占锁,因此如果更改等待状态失败,那说明该结点原本就不是Node.CONDITION状态,表示结点早已经取消等待了,则直接返回false,表示转移失败。
  2. CAS成功,则表示该结点是处于等待状态,那么调用enq将结点添加到同步队列尾部,返回添加结点在同步队列中的前驱结点。
  3. 获取前驱结点的状态ws。如果ws大于0,则表示前驱已经被取消了或者将ws改为Node.SIGNAL失败,表示前驱可能在此期间被取消了,那么调用unpark方法唤醒被转移结点中的线程,好让它从await中的等待中醒来;否则,那就由它的前驱结点在获取锁之后释放锁时再唤醒。返回true。
/**
 * 将结点从条件队列转移到同步队列,并尝试唤醒
 *
 * @param node 被转移的结点
 * @return 如果成功转移,返回true;失败则返回false
 */
final boolean transferForSignal(Node node) {
    /*1 尝试将结点的等待状态变成0,表示取消等待
    如果更改等待状态失败,那说明一定是原本就不是Node.CONDITION状态,表示结点早已经取消等待了,则返回false。
    这里不存在并发的情况,因为调用线程此时已经获取了独占锁*/
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    /*2 将结点添加到同步队列尾部,返回添加结点的前驱结点*/
    Node p = enq(node);
    //获取前驱结点的状态ws
    int ws = p.waitStatus;
    /*3 如果ws大于0 表示前驱已经被取消了 或者 将ws改为Node.SIGNAL失败,表示前驱可能在此期间被取消了
    则调用unpark方法唤醒被转移结点中的线程,好让它从await中的等待唤醒(后续尝试获取锁)
    否则,那就由它的前驱结点获取锁之后释放锁时再唤醒。
    */
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    //返回true
    return true;
}
复制代码

被唤醒后的线程(无论是在signal中被唤醒的、还是由于同步队列前驱释放锁被唤醒的、还是由于在等待时因为中断而被唤醒的),都将从await()方法中的while循环中退出,下一步将调用同步器的acquireQueued()方法加入到获取锁的竞争中。

4.2 signalAll通知全部线程

signalAll方法,相当于对等待队列中的每个结点均执行一次signal方法,效果就是将等待队列中所有结点全部移动到同步队列中,并尝试唤醒每个结点的线程,让他们竞争锁。大概步骤为:

  1. 检查调用signalAll方法的线程是否是持有锁的线程,如果不是则直接抛出IllegalMonitorStateException异常。
  2. 调用doSignalAll方法将条件队列中的所有等待状态的结点转移至同步队列尾部,然后根据条件可能会尝试唤醒该结点对应的线程,相当于清空了条件队列。
/**
 * Conditon中的方法
 * 将次Condition中的所有等待状态的结点 从条件队列移动到同步队列中。
 *
 * @throws IllegalMonitorStateException 如果当前调用线程不是获取锁的线程,则抛出异常
 */
public final void signalAll() {
    /*1 首先调用isHeldExclusively检查当前调用线程是否是持有锁的线程
     * isHeldExclusively方法需要我们重写
     * */
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //获取头结点
    Node first = firstWaiter;
    /*2 如果不为null,调用doSignalAll方法将条件队列中的所有等待状态的结点转移至同步队列尾部,
    然后根据条件可能会尝试唤醒该结点对应的线程,相当于清空了条件队列。*/
    if (first != null)
        doSignalAll(first);
}
复制代码

4.2.1 doSignalAll移除-转移全部结点

移除并尝试转移条件队列的所有结点,实际上会将条件队列清空。对每个结点调用transferForSignal方法。

/**
 * Conditon中的方法
 * 移除并尝试转移条件队列的所有结点,实际上会将条件队列清空
 *
 * @param first 条件队列头结点
 */
private void doSignalAll(Node first) {
    //头结点尾结点都指向null
    lastWaiter = firstWaiter = null;
    /*do while 循环转移结点*/
    do {
        //next保存当前结点的后继
        Node next = first.nextWaiter;
        //当前结点的后继引用置空
        first.nextWaiter = null;
        //调用transferForSignal尝试转移结点,就算失败也没关系,因为transferForSignal一定会对所有的结点都尝试转移
        //可以看出来,这里的转移是一个一个的转移的
        transferForSignal(first);
        //first指向后继
        first = next;
    } while (first != null);
}
复制代码

5 Condition的应用

有了Condition,,在配合Lock就能实现可控制的多线程案例,让多线程按照我们业务需求去执行,比如下面的常见案例!

5.1 生产消费案例

使用Lock和Condition实现简单的生产消费案例,生产一个产品就需要消费一个产品,一次最多只能生产、消费一个产品。

常见实现是:如果存在商品,那么生产者等待,并唤醒消费者;如果没有商品,那么消费者等待,并唤醒生产者!

public class ProducerAndConsumer {
    public static void main(String[] args) {
        Resource resource = new Resource();
        Producer producer = new Producer(resource);
        Consumer consumer = new Consumer(resource);
        //使用线程池管理线程
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 4, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        //两个生产者两个消费者
        threadPoolExecutor.execute(producer);
        threadPoolExecutor.execute(consumer);
        threadPoolExecutor.execute(producer);
        threadPoolExecutor.execute(consumer);
        threadPoolExecutor.shutdown();
    }

    /**
     * 产品资源
     */
    static class Resource {
        private String name;
        private int count;
        //标志位
        boolean flag;
        //获取lock锁,lock锁的获取和释放需要代码手动操作
        ReentrantLock lock = new ReentrantLock();
        //从lock锁获取一个condition,用于生产者线程在此等待和唤醒
        Condition producer = lock.newCondition();
        //从lock锁获取一个condition,用于消费者线程在此等待和唤醒
        Condition consumer = lock.newCondition();

        void set(String name) {
            //获得锁
            lock.lock();
            try {
                while (flag) {
                    try {
                        System.out.println("有产品了--" + Thread.currentThread().getName() + "生产等待");
                        //该生产者线程,在producer上等待
                        producer.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                ++count;
                this.name = name;
                System.out.println(Thread.currentThread().getName() + "生产了" + this.name + +count);
                flag = !flag;
                //唤醒在consumer上等待的消费者线程,这样不会唤醒等待的生产者
                consumer.signalAll();
            } finally {
                //释放锁
                lock.unlock();
            }
        }

        void get() {
            lock.lock();
            try {
                while (!flag) {
                    try {
                        System.out.println("没产品了--" + Thread.currentThread().getName() + "消费等待");
                        //该消费者线程,在consumer上等待
                        consumer.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "消费了" + this.name + count);
                flag = !flag;
                //唤醒在producer监视器上等待的生产者线程,这样不会唤醒等待的消费者
                producer.signalAll();
            } finally {
                lock.unlock();
            }
        }
    }

    /**
     * 消费者行为
     */
    static class Consumer implements Runnable {
        private Resource resource;

        public Consumer(Resource resource) {
            this.resource = resource;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //调用消费方法
                resource.get();
            }
        }
    }

    /**
     * 生产者行为
     */
    static class Producer implements Runnable {
        private Resource resource;

        public Producer(Resource resource) {
            this.resource = resource;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //调用生产方法
                resource.set("面包");
            }
        }
    }
}
复制代码

5.2 实现商品仓库

使用Lock和Condition实现较复杂的的生产消费案例,实现一个中间仓库,产品被存储在仓库之中,可以连续生产、消费多个产品。

这个实现,可以说就是简易版的消息队列!

/**
 * @author lx
 */
public class BoundedBuffer {

    public static void main(String[] args) {
        Resource resource = new Resource();
        Producer producer = new Producer(resource);
        Consumer consumer = new Consumer(resource);
        //使用线程池管理线程
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 4, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        //两个生产者两个消费者
        threadPoolExecutor.execute(producer);
        threadPoolExecutor.execute(consumer);
        threadPoolExecutor.execute(producer);
        threadPoolExecutor.execute(consumer);
        threadPoolExecutor.shutdown();
    }

    /**
     * 产品资源
     */
    static class Resource {

        // 获得锁对象
        final Lock lock = new ReentrantLock();
        // 获得生产监视器
        final Condition notFull = lock.newCondition();
        // 获得消费监视器
        final Condition notEmpty = lock.newCondition();
        // 定义一个数组,当作仓库,用来存放商品
        final Object[] items = new Object[100];
        /*
         * putpur:生产者使用的下标索引;
         * takeptr:消费者下标索引;
         * count:用计数器,记录商品个数
         */
        int putptr, takeptr, count;

        /**
         *  生产方法
         * @param x
         * @throws InterruptedException
         */
        public void put(Object x) throws InterruptedException {
            // 获得锁
            lock.lock();
            try {
                // 如果商品个数等于数组的长度,商品满了将生产将等待消费者消费
                while (count == items.length) {
                    notFull.await();
                }
                // 生产索引对应的商品,放在仓库中
                Thread.sleep(50);
                items[putptr] = x;
                // 如果下标索引加一等于数组长度,将索引重置为0,重新开始
                if (++putptr == items.length) {
                    putptr = 0;
                }
                // 商品数加1
                ++count;
                System.out.println(Thread.currentThread().getName() + "生产了" + x + "共有" + count + "个");
                // 唤醒消费线程
                notEmpty.signal();
            } finally {
                // 释放锁
                lock.unlock();
            }
        }

        /**
         * 消费方法
         * @return
         * @throws InterruptedException
         */
        public Object take() throws InterruptedException {
            //获得锁
            lock.lock();
            try {
                //如果商品个数为0.消费等待
                while (count == 0) {
                    notEmpty.await();
                }
                //获得对应索引的商品,表示消费了
                Thread.sleep(50);
                Object x = items[takeptr];
                //如果索引加一等于数组长度,表示取走了最后一个商品,消费完毕
                if (++takeptr == items.length)
                //消费索引归零,重新开始消费
                {
                    takeptr = 0;
                }
                //商品数减一
                --count;
                System.out.println(Thread.currentThread().getName() + "消费了" + x + "还剩" + count + "个");
                //唤醒生产线程
                notFull.signal();
                //返回消费的商品
                return x;
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    }

    /**
     * 生产者行为
     */
    static class Producer implements Runnable {
        private Resource resource;

        public Producer(Resource resource) {
            this.resource = resource;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    resource.put("面包");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    /**
     * 消费者行为
     */
    static class Consumer implements Runnable {
        private Resource resource;

        public Consumer(Resource resource) {
            this.resource = resource;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    resource.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }
}
复制代码

5.3 输出ABCABC……

编写一个程序,开启3 个线程,这三个线程的name分别为A、B、C,每个线程将自己的名字 在屏幕上打印10 遍,要求输出的结果必须按名称顺序显示,例如:ABCABCABC…

这个案例中,我们需要手动控制相关线程在指定线程之前执行。

/**
 * @author lx
 */
public class PrintABC {
    ReentrantLock lock = new ReentrantLock();
    Condition A = lock.newCondition();
    Condition B = lock.newCondition();
    Condition C = lock.newCondition();
    /**
     * flag标志,用于辅助控制顺序,默认为1
     */
    private int flag = 1;

    public void printA(int i) {
        lock.lock();
        try {
            while (flag != 1) {
                try {
                    A.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " " + i);
            flag = 2;
            B.signal();
        } finally {
            lock.unlock();
        }
    }

    public void printB(int i) {
        lock.lock();
        try {
            while (flag != 2) {
                try {
                    B.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " " + i);
            flag = 3;
            C.signal();
        } finally {
            lock.unlock();
        }
    }

    public void printC(int i) {
        lock.lock();
        try {
            while (flag != 3) {
                try {
                    C.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " " + i);
            System.out.println("---------------------");
            flag = 1;
            A.signal();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        PrintABC testABC = new PrintABC();
        Thread A = new Thread(new A(testABC), "A");
        Thread B = new Thread(new B(testABC), "B");
        Thread C = new Thread(new C(testABC), "C");
        A.start();
        B.start();
        C.start();
    }

    static class A implements Runnable {
        private PrintABC testABC;

        public A(PrintABC testABC) {
            this.testABC = testABC;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                testABC.printA(i + 1);
            }
        }
    }

    static class B implements Runnable {
        private PrintABC testABC;

        public B(PrintABC testABC) {
            this.testABC = testABC;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                testABC.printB(i + 1);
            }
        }
    }

    static class C implements Runnable {
        private PrintABC testABC;

        public C(PrintABC testABC) {
            this.testABC = testABC;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                testABC.printC(i + 1);
            }
        }
    }
}
复制代码

6 AQS的总结

AQS是JUC中实现同步组件的基础框架,有了AQS我们自己也能比较轻松的实现自定义的同步组件。

AQS中提供了同步队列的实现,用于实现锁的获取和释放,没有获取到锁的线程将进入同步队列排队等待,是实现线程同步的基础。

AQS内部还提供了条件队列的实现,条件队列用于实现线程之间的主动等待、唤醒机制,是实现线程 有序可控 同步的不可缺少的部分。

一个锁对应一个同步队列,对应多个条件变量,每个条件变量有自己的一个条件队列,这样就可以实现按照业务需求让不同的线程在不同的条件队列上等待,相对于Synchronized的只有一个条件队列,功能更加强大!

最后,当我们深入源码时,发现对于最基础的同步支持,比如可见性、原子性、线程等待、唤醒等操作,AQS也是调用的其他工具、或者利用了其他特性:

  1. 同步状态state被设置为volatile类型,这样在获取、更新时保证了可见性,还可以禁止重排序!
  2. 使用CAS来更新变量,来保证单个变量的复合操作(读-写)具有原子性!而CAS方法内部又调用了Unsafe的方法。这个Unsafe类,实际上是比AQS更加底层的底层框架,或者可以认为是AQS框架的基石。
  3. CAS操作在Java中的最底层的实现就是Unsafe类提供的,它是作为Java语言与Hospot源码(C++)以及底层操作系统沟通的桥梁,可以去了解Unsafe的一些操作。
  4. 对于线程等待、唤醒,是调用了LockSupport的park、unpark方法,如果去看LockSupport的源码,那么实际上最终还是调用Unsafe类中的方法!

AQS框架是JUC中的同步组件的基石,如果再去尝试寻找构建AQS的基石的话,通过AQS的Java源码我们可以发现就是:

  1. volatile修饰符——保证数据可见性。
  2. CAS(UNSAFE)——保证State变量读-写-改操作的原子性与可见性。
  3. LockSupport#park、LockSupport(UNSAFE)——线程挂起与唤醒。

整个AQS并没有用到Synchronized关键字。

学习了AQS,对于我们后续将会进行的Lock锁等JUC同步组件的实现分析将会大有帮助!

相关文章:

  1. LockSupport:JUC—LockSupport以及park、unpark方法底层源码深度解析
  2. volatile:Java中的volatile实现原理深度解析以及应用
  3. CAS:Java中的CAS实现原理解析与应用
  4. UNSAFE:JUC—Unsafe类的原理详解与使用案例

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

猜你喜欢

转载自juejin.im/post/7068482899929989134