Java多线程(八)生产者消费者——Condition和精准唤醒

2,273 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

什么是Condition

对于任意一个java对象,它都拥有一组定义在java.lang.Object上监视器方法,包括wait(),wait(long timeout),notify(),notifyAll(),这些方法配合synchronized关键字一起使用可以实现等待/通知模式。同样,Condition接口也提供了类似Object监视器的方法,通过与Lock配合来实现等待/通知模式。可以看一下Object类的监视器方法和Condition接口的对比:

在这里插入图片描述

Condition解决生产者消费者问题

假设生产者可以生产票,但是现存的票只能有一张,只有顾客买走了才能再生产一张票,因此可以用Condition来保证同步。havenum表示有票,需要生产者等待;nonum表示没票,需要消费者等待。代码如下:

class tickets{
    private int num = 0;
    ReentrantLock lock = new ReentrantLock();

    Condition nonum = lock.newCondition();
    Condition havenum = lock.newCondition();


    public void put() throws InterruptedException {
        lock.lock();
        try{
            while(num==1)
            {
                nonum.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName()+" 生产了一份,现存数量是 "+num);
            havenum.signalAll();
        }
        finally {
            lock.unlock();
        }
    }

    public void take() throws InterruptedException {
        lock.lock();
        try {
            while(num==0)
            {
                havenum.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName()+" 消耗了一份,现存数量是 "+num);
            nonum.signalAll();
        }
        finally {
            lock.unlock();
        }
    }
}

在这里插入图片描述

Condition 精准唤醒

上一篇文章Java多线程(七)生产者消费者——wait && notify && 虚假唤醒 - 掘金 (juejin.cn)中说到可以用Condition机制进行按顺序唤醒,通过上面的例子我们也可以发现使用不同的Condition对象可以唤醒不同的线程,使用这一机制就可以做到精准唤醒。

class Aweaken
{
    ReentrantLock lock = new ReentrantLock();
    int num = 1;
    Condition conditionA = lock.newCondition();
    Condition conditionB = lock.newCondition();
    Condition conditionC = lock.newCondition();

    public void weakA()
    {
        lock.lock();
        try {
            while(num != 1)
            {
                conditionA.await();
            }
            num = 2;
            System.out.println("现在是线程 "+Thread.currentThread().getName()+", 下一个应该是线程B");
            conditionB.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void weakB()
    {
        lock.lock();
        try {
            while(num != 2)
            {
                conditionB.await();
            }
            num = 3;
            System.out.println("现在是线程 "+Thread.currentThread().getName()+", 下一个应该是线程C");
            conditionC.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void weakC()
    {
        lock.lock();
        try {
            while(num != 3)
            {
                conditionC.await();
            }
            num = 1;
            System.out.println("现在是线程 "+Thread.currentThread().getName()+", 下一个应该是线程A");
            conditionA.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class PCold {

    public static void main(String[] args) {
        Aweaken b = new Aweaken();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                b.weakA();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                b.weakB();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                b.weakC();
            }
        },"C").start();

    }
}

Condition 实现分析

等待队列

ConditionObject的等待队列是一个FIFO队列,队列的每个节点都是等待在Condition对象上的线程的引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await(),那么该线程就会释放锁,构成节点加入等待队列并进入等待状态。

从下图可以看出来Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并更新尾节点即可。上述节点引用更新过程没有使用CAS机制,因为在调用await()的线程必定是获取了锁的线程,该过程由锁保证线程的安全。

一个Lock(同步器)拥有一个同步队列和多喝等待队列(如下图所示) 在这里插入图片描述

等待

调用Condition的await()方法,会使得当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()返回时,当前线程一定是获取了Condition相关联的锁。

线程触发await()这个过程可以看作是同步队列的首节点(当前线程肯定是成功获得了锁,因此一定是在同步队列的首节点)移动到了Condition的等待队列的尾节点,并释放同步状态进入等待状态,同时会唤醒同步队列的后继节点。

在这里插入图片描述

唤醒

调用Condition的signal()方法将会唤醒再等待队列中的首节点,该节点也是到目前为止等待时间最长的节点。调用Condition的signalAll()方法,将等待队列中的所有节点全部唤醒,相当于将等待队列中的每一个节点都执行一次signal()。

在这里插入图片描述