Condition的await和signal等待/通知机制

Condition简介

任何一个java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如 wait(),wait(long timeout),wait(long timeout, int nanos)notify(),notifyAll()几个方法实现等待/通知机制。同样的, 在 java Lock体系下依然会有同样的方法实现等待/通知机制。

从整体上来看Object类提供的wait和notify/notify方法是与对象监视器monitor配合完成线程间的等待/通知机制,属于JVM底层实现。而Condition与Lock配合完成的等待通知机制,属于Java语言级别的,具有更高的可控制性和扩展性。

两者除了在使用方式上不同外,在功能特性上还是有很多的不同:

  1. Condition能够支持不响应中断,而通过使用Object方式不支持;
  2. Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
  3. Condition能够支持超时时间的设置,而Object不支持

Condition方法

参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法:

针对Object的wait方法
void await() throws InterruptedException   //当前线程进入等待状态,同Object.wait(),直到被中断或唤醒
long awaitNanos(long nanosTimeout)   // 当前线程进入等待状态,不响应中断,直到被唤醒
boolean await(long time, TimeUnit unit)throws InterruptedException //  同Object.wait(long timeout),多了自定义时间单位,在中断、超时、被唤醒三种情况下返回
boolean awaitUntil(Date deadline) throws InterruptedException  // 支持设置截止时间
针对Object的notify/notifyAll方法
void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。 
void signalAll():将所有等待在condition上的线程全部转移到同步队列中

Condition实现原理分析

等待队列

创建一个 condition 对象是通过lock.newCondition(),而这个方法实际上是会new出一个ConditionObject对象,该类是AQS的一个内部类。前面我们说过,condition是要和lock配合使用的也就是condition和Lock是绑定在一起的,而lock的实现原理又依赖于AQS,自然而然ConditionObject作为AQS的一个内部类无可厚非。

我们知道在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程尾插入到同步队列,同样的,condition内部也是使用同样的方式,内部维护了一个等待队列,所有调用condition.await()方法的线程会加入到等待队列中,并且线程状态转换为等待状态。

    public Condition newCondition() {
        return sync.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() { }

从中我们就可以看出来ConditionObject通过持有等待队列的头(Node firstWaiter)尾(Node lastWaiter)指针来管理等待队列。需要注意的是Node类复用了在AQS中的Node类

    static final class Node {
    	...
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
        ...
    }

Node类有这样一个属性Node nextWaiter;(后继节点),进一步说明,等待队列是一个单向队列,而在之前说AQS时知道同步队列是一个双向队列。

我们通过代码来进一步验证等待队列的结构:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class WaitTestDemo {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        for(int i = 0;i < 10;i++){
            new Thread(()->{
                lock.lock();
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }).start();
        }
    }
}

新建了10个线程,没有线程先获取锁,然后调用condition.await方法释放锁将当前线程加入到等待队列中,通过 debug控制当走到第10个线程的时候查看firstWaiter即等待队列中的头结点,debug模式下情景图如下:
在这里插入图片描述
从这个图我们可以很清楚的看到这样几点:

  1. 调用condition.await方法后线程依次尾插入到等待队列中,如图队列中的线程引用依次为Thread-0,Thread-1,Thread-2…Thread-8;
  2. 等待队列是一个单向队列。通过我们的猜想然后进行实验验证,我们可以得出等待队列的示意图如下图所示:

    同时还有一点需要注意的是:我们可以多次调用lock.newCondition()方法创建多个condition对象,也就是一个lock可以持有多个等待队列。而在之前利用Object的方式实际上是指在对象Object对象监视器上只能拥有一个同步队列和一个等待队列,而并发包中的Lock拥有一个同步队列和多个等待队列

如图所示,ConditionObject是AQS的内部类,因此每个ConditionObject能够访问到AQS提供的方法,相当于每个Condition都拥有所属同步器的引用。

Condition应用场景—用Condition来实现有界队列

有界队列是一种特殊的队列,当队列为空时,队列的获取(删除)操作将会阻塞获取(删除)线程,直到队列中有新增 元素;当队列已满时,队列的插入操作将会阻塞插入线程,直到队列出现空位。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class BoundedQueue<T> {
    private Object[] items;
    // 队列中当前元素个数
    private int count;
    private Lock lock = new ReentrantLock();
    private Condition empty = lock.newCondition();
    private Condition full = lock.newCondition();

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

    // 添加元素方法,如果当前队列已满,
    // 则添加线程进入等待状态,直到有空位被唤醒
    public void addThraed(T t,int addIndex){
        lock.lock();
        try {
            // 当前队列已满,添加线程进入等待状态
            while (count == items.length){
                try {
                    full.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            items[addIndex] = t;
            count++;
            empty.signal();
        }finally {
            lock.unlock();
        }
    }

    // 删除元素方法,如果当前队列为空,
    // 则移除线程进入等待状态直到到队列不为空时被唤醒
    public T removeThread(int removeIndex) {
        lock.lock();
        try {
            // 当队列为空时,移除线程进入等待状态
            while (count == 0) {
                try {
                    empty.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Object x = items[removeIndex];
            count--;
            full.signal();
            return (T) x;
        } finally {
            lock.unlock();
        }
    }
}

await实现原理

当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列,如果该线程能够从await()方法返回的话, 一定是该线程获取了与condition相关联的lock。await()方法源码为:

        /**
         * Implements interruptible condition wait.
         * <ol>
         * <li> If current thread is interrupted, throw InterruptedException.
         * <li> Save lock state returned by {@link #getState}.
         * <li> Invoke {@link #release} with saved state as argument,
         *      throwing IllegalMonitorStateException if it fails.
         * <li> Block until signalled or interrupted.
         * <li> Reacquire by invoking specialized version of
         *      {@link #acquire} with saved state as argument.
         * <li> If interrupted while blocked in step 4, throw InterruptedException.
         * </ol>
         */
        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
              // 1.将当前线程包装为Node,尾插到等待队列中 
            Node node = addConditionWaiter();
             // 2.释放当前线程所占用的lock,释放后会唤醒同步队列中的下一个节点 
            long 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);
        }

当当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中,直至被signal/signalAll()后会使得当前线程从等待队列中移至到同步队列中去,直到获得了lock后才 会从await方法返回,或者在等待时被中断会做中断处理。

那么关于这个实现过程我们会有这样几个问题:

  1. 是怎样将当前线程添加到等待队列中去的?
  2. 释放锁的过程?
  3. 怎样才能从await方法退出?

在第1步中调用addConditionWaiter()将当前线程添加到等待队列中,该方法源码为:

       /**
         * Adds a new waiter to wait queue.
         * @return its new wait node
         */
        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            // 将当前线程包装为Node 
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
            	 // 尾插入等待队列 
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

将当前节点包装成Node,如果等待队列的firstWaiter为null的话(等待队列为空队列), 则将firstWaiter指向当前的Node;否则,更新lastWaiter(尾节点)即可。就是通过尾插入的方式将当前线程封装的Node插入到等待队列中即可,同时可以看出等待队列是一个不带头结点的链式队列,我们知道AQS的同步队列是一个带头结点的链式队列,这是两者的一个区别。

将当前节点插入到等待队列之后,会使当前线程释放lock,由 fullyRelease()方法实现,fullyRelease源码为:

    /**
     * Invokes release with current state value; returns saved state.
     * Cancels node and throws exception on failure.
     * @param node the condition node for this wait
     * @return previous sync state
     */
    final long fullyRelease(Node node) {
        boolean failed = true;
        try {
            long savedState = getState();
             // 调用AQS的释放同步状态方法release() 
            if (release(savedState)) {
             	// 成功释放锁状态 
                failed = false;
                return savedState;
            } else {
            	 // 释放同步状态失败抛出异常 
                throw new IllegalMonitorStateException();
            }
        } finally {
        	// 释放同步状态失败后将当前节点状态置为取消状态 
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

调用AQS的模板方法release方法释放AQS的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,若失败的话就抛出异常。

那么怎样从await方法退出? await方法有这样一段逻辑:

 while (!isOnSyncQueue(node)) {
            	// 当前节点不在同步队列时被阻塞进入等待状态
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }

很显然,当线程第一次调用condition.await()方法时,会进入到这个while()循环中,然后通过LockSupport.park(this)方法使得当前线程进入等待状态,那么要想退出这个await方法第一个前提条件自然而然的是要先退出这个while循 环,出口就只剩下两个地方:1. 逻辑走到break退出while循环;2. while循环中的逻辑判断为false

代码出现第1种情况的条件是当前等待的线程被中断后代码会走到break退出,第二种情况是当前节点被移动到了同步队列中 (即另外线程调用的condition的signal或者signalAll方法),while中逻辑判断为false后结束while循环。总结下,就是当前线程被中断或者调用condition.signal/condition.signalAll方法使当前节点移动到了同步队列后这是当前线程退出await方法的前提条件

当退出while循环后就会调用acquireQueued(node, savedState),这个方法在介绍AQS的底层实现时说过了,该方法的作用是在自旋过程中线程不断尝试获取同步状态,直至成功(线程获取到lock)。这 样也说明了退出await方法必须是已经获得了condition引用(关联)的lock。

如上图所示:调用condition.await()方法的线程必须是已经获得了lock,也就是当前线程是同步队列中的头结点。调用该方法后会使得当前线程所封装的Node尾插入到等待队列中。

signal/signalAll实现原理

调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock。按照等待队列是先进先出(FIFO)的原则,等待队列的头节点必然会是等待时间长的节点, 也就是每次调用condition的signal方法是将头节点移动到同步队列中。signal方法源码为:

        /**
         * Moves the longest-waiting thread, if one exists, from the
         * wait queue for this condition to the wait queue for the
         * owning lock.
         *
         * @throws IllegalMonitorStateException if {@link #isHeldExclusively}
         *         returns {@code false}
         */
        public final void signal() {
        	 // 当前线程是否已经获取lock 
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
                 // 获取等待队列的第一个节点,之后的操作都是针对这个节点 
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

下面我们来看看doSignal方法做了些什么事情,doSignal()方法源码为:

        /**
         * Removes and transfers nodes until hit non-cancelled one or
         * null. Split out from signal in part to encourage compilers
         * to inline the case of no waiters.
         * @param first (non-null) the first node on condition queue
         */
        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                    // 将头结点从等待队列中移除 
                first.nextWaiter = null;
                 // transferForSignal方法对头结点做真正的处理
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

真正对头节点做处理的逻辑在transferForSignal()方法,该方法源码为:

    /**
     * Transfers a node from a condition queue onto sync queue.
     * Returns true if successful.
     * @param node the node
     * @return true if successfully transferred (else the node was
     * cancelled before signal)
     */
    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
          // 首先将节点状态更新为0 
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
          // 将节点使用enq方法尾插到同步队列中 
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

这段代码主要做了两件事情:

  1. 将头结点的状态更改为CONDITION;
  2. 调用enq()方法,将该节点尾插入到同步队列中。

现在我们可以得出结论:调用condition的signal的前提条件是当前线程已经获取了lock,该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列,而移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出。signal执行示意图如下图:

signalAll()方法底层原理

signalAll()sigal()方法的区别体现在doSignalAll()方法上,前面我们已经知道doSignal方法只会对等待队列的头节点进行操作,而doSignalAll的源码为:

        /**
         * Moves all threads from the wait queue for this condition to
         * the wait queue for the owning lock.
         *
         * @throws IllegalMonitorStateException if {@link #isHeldExclusively}
         *         returns {@code false}
         */
        public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }
        
        /**
         * Removes and transfers all nodes.
         * @param first (non-null) the first node on condition queue
         */
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

该方法只不过是将等待队列中的每一个节点都移入到同步队列中,即“通知”当前调用condition.await()方法的每一个线程。

Condition机制实现生产-消费者模型

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Goods{
    private String name;
    private int count;
    private int maxCount;
    private Lock lock = new ReentrantLock();
    // 消费者等待队列
    private Condition consumerCondition = lock.newCondition();
    // 生产者等待队列
    private Condition producerCondition = lock.newCondition();
    public Goods(int maxCount) {
        this.maxCount = maxCount;
    }

    @Override
    public String toString() {
        return "Goods{" +
                "name='" + name + '\'' +
                ", count=" + count +
                '}';
    }

    /**
    生产者方法
    @param name 设置商品名称
     */
    public void setGoods(String name){
        lock.lock();
        try{
            // 商品数量达到最大值,生产者线程进入生产者等待队列
            while (maxCount == count){
                try {
                    System.out.println(Thread.currentThread().getName()
                        +"还有很多商品,歇会~");
                    producerCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.name = name;
            count++;
            System.out.println(Thread.currentThread().getName()+"生产"
                +toString());
            // 唤醒处于消费者的线程
            consumerCondition.signalAll();
        }finally {
            lock.unlock();
        }
    }

    /**
     消费者方法
     */
    public void getGoods(){
        lock.lock();
        try{
            while (0 == count){
                try {
                    System.out.println(Thread.currentThread().getName()
                            +"还没有商品~");
                    consumerCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count--;
            System.out.println(Thread.currentThread().getName()+"消费"
                    +toString());
            // 唤醒所有生产者的线程
            producerCondition.signalAll();
        }finally {
            lock.unlock();
        }
    }
}

class Producer implements Runnable{
    private Goods goods;

    public Producer(Goods goods) {
        this.goods = goods;
    }

    @Override
    public void run() {
        while(true){
            this.goods.setGoods("限量版Mac口红");
        }
    }
}
class Consumer implements Runnable{
    private Goods goods;

    public Consumer(Goods goods) {
        this.goods = goods;
    }

    @Override
    public void run() {
        while(true){
            this.goods.getGoods();
        }
    }
}

public class Cache {
    public static void main(String[] args) {
        Goods goods = new Goods(10);
        Producer producer = new Producer(goods);
        Consumer consumer = new Consumer(goods);
        List<Thread> list = new ArrayList<>();

        // 启动5个生产者线程
        for (int i = 0;i < 5;i++){
            Thread thread = new Thread(producer,"生产者"+i);
            list.add(thread);
        }
        // 启动10个消费者线程
        for (int i = 0;i < 10;i++){
            Thread thread = new Thread(consumer,"消费"+i);
            list.add(thread);
        }
        for (Thread thread : list){
            thread.start();
        }
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值