前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发编程的艺术-为何创建两个Condition接口实现线程的通知等待机制

Java并发编程的艺术-为何创建两个Condition接口实现线程的通知等待机制

作者头像
Fisherman渔夫
发布2020-02-18 16:36:12
4730
发布2020-02-18 16:36:12
举报
文章被收录于专栏:渔夫渔夫渔夫

一、引言

 不知道你看《Java并发编程的艺术》此书的5.6Condition接口这一节内容时,在查看BoundedQueue.java代码的时候是否有疑问:为何有两个Condition对象:

private Condition    notEmpty = lock.newCondition();
private Condition    notFull = lock.newCondition();

 这个疑问是否来源于,既然是一个锁,为何需要两个Conditon接口来进行消费者-生产者模式下的线程管理呢?下面我先给出完整的书上源码,以及我个人对于源码的解读,其次再来回答这个问题。

二、代码分析

public class BoundedQueue<T> {
    private Object[]    items;
    // 添加的下标,删除的下标和数组当前数量
    private int addIndex, removeIndex, count;//注意默认初始值都是0
    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) throws InterruptedException {
            lock.lock();//写之前需要先获取锁资源
            try {
                    while (count == items.length)//意味着队列已满,不能够再添加元素
                            notFull.await();//使当前线程进入休眠状态
                    items[addIndex] = t;//以下是环形队列增加元素的代码,相信我说了此为环形,你一定能够理解以下代码
                    if (++addIndex == items.length)
                            addIndex = 0;
                    ++count;
                    notEmpty.signal();//唤醒一个等待当前锁的线程
            } finally {
                    lock.unlock();//最终总是要释放锁资源的。
            }
    }
    // 由头部删除一个元素,如果数组空,则删除线程进入等待状态,直到有新添加元素
    @SuppressWarnings("unchecked")
    public T remove() throws InterruptedException {
            lock.lock();
            try {
                    while (count == 0)//判断当前队列为空
                            notEmpty.await();
                    Object x = items[removeIndex];//以下为环形队列删去元素的相关代码实现
                    if (++removeIndex == items.length)
                            removeIndex = 0;
                    --count;
                    notFull.signal();
                    return (T) x;
            } finally {
                    lock.unlock();//
            }
    }
}

代码分析:  首先我给出两个Condition接口对象调用相关方法的时机: notEmpty.signal()以及notFull.signial()方法分别是在判断当前队列不为空,当前队列不为满时执行的。 notEmpty.await()以及notFull.await()方法则是分别在当前队列为空时调用的,当前队列为满的时候调用的。

其次我来说明使用两个Condition接口对象实现线程管理的原因:  其主要目的就是方便地在调用线程唤醒、休眠操作的时候,我们通过其对象以及其方法名就能够知道此时满足什么条件,这正如上面对于4种方法调用时机的分析所示(比方说我们在代码种读到:notEmpty.signal(),那么马上就可猜测此时条件为队列不为空)。也就是说,将上述代码改成只有一个Condition接口对象实例是完全没有问题的。希望你理解以下的一个知识点:唤醒、休眠操作的对象总是当前占据锁资源的线程,而不是Condition接口实例。

 只有一个Condition接口实例的版本:将notFull对象删除,仅仅留下notEmpty对象。并且附上了额外的测试代码。

/**
 * @author Fisherman
 */

public class BoundedQueue<T> {
    private Object[] items;
    // 添加的下标,删除的下标和数组当前数量
    private int addIndex, removeIndex, count;//注意默认初始值都是0
    private Lock lock = new ReentrantLock();//锁结构为独占的重入锁
    private Condition notEmpty = lock.newCondition();

    public BoundedQueue(int size) {
        items = new Object[size];//队列的构造器
    }

    // 添加一个元素,如果数组满,则添加线程进入等待状态,直到有"空位"
    public void add(T t) throws InterruptedException {
        lock.lock();//写之前需要先获取锁资源
        try {
            while (count == items.length)//意味着队列已满,不能够再添加元素
                notEmpty.await();
            items[addIndex] = t;
            if (++addIndex == items.length)
                addIndex = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    // 由头部删除一个元素,如果数组空,则删除线程进入等待状态,直到有新添加元素
    @SuppressWarnings("unchecked")
    public T remove() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[removeIndex];
            if (++removeIndex == items.length)
                removeIndex = 0;
            --count;
            notEmpty.signal();
            return (T) x;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        /**
         * 使用以下代码进行测试:
         */
        BoundedQueue boundedQueue = new BoundedQueue(5);

        new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                try {
                    boundedQueue.add(1);
                    Thread.sleep(500);
                    System.out.printf("增加第%d号元素队列元素成功\n",i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }


        }).start();

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                try {
                    Thread.sleep(800);
                    boundedQueue.remove();
                    System.out.printf("移除第%d号元素队列元素成功\n",i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }).start();
    }
}

三、补充

 两个Condition接口实例对象的设计更像是我们平常生活中:生产者和消费者所占得等待队列不一样,生产者走在商场的后门进行生产和等待,消费者在商场的柜台进行排队购买,如下图所示:

在这里插入图片描述
在这里插入图片描述

 相当于我们使生产者和消费者在两个不同相隔离的等待队列中进行相关等待操作。所以说第二个只使用一个上锁结构的代码块可能会造成代码执行效率的降低,举个例子:消费和生产都是需要耗时的,所以会有以下可能的情况出现:  由于队列资源的有限,有负责生产的线程加入了等待队列中,又有负责消费的线程也进入了同一个等待队伍中,由于调用的方法是signal方法,不会唤醒所有等待的线程,而是会地唤醒一个等待时间最长的线程(也就是等待队列中的首节点),而首节点可能是消费者节点也可能是生产者节点,所以这样一来,一个消费线程消费完毕后,唤醒的线程可能还是一个消费线程,但是此时并没有新的产品可以提供其消费,所以其又会执行signal方法,而这个方法又不一定唤醒负责生产的等待线程。虽然最终是能够唤醒生产线程,但是效率上却是降低了。所以,使用两个Condition接口实现的生产者-消费者模式最大的好处就是可以在调用signal方法的时候精确地唤醒等待或生产线程,而不是有随机性地唤醒。多创建的线程还是体现了**以空间换时间的思想。**但是有人会说,使用signalAll方法调用,就不会有这样的问题,但是线程signalAll方法还是会相对地比signal方法效率低。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、引言
  • 二、代码分析
  • 三、补充
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档