《蹲坑也能进大厂》多线程系列 - 你知道wait/notify的加强版吗|8月更文挑战

1,585 阅读5分钟

前面章节我们学习了wait/notify的相关知识,也知道他们的组合拳可以让线程避免自旋,从而降低性能损耗,减少对CPU的浪费。今天要介绍的是它的威力加强版,也就是Condition的使用,整体知识点不是很多,相信小伙伴们可以轻松掌握。

一、Condition简介

在Java1.5之前,开发者主要使用await/notify来实现线程之间的协作,而在1.5版本中,Condition正式诞生,它的出现其实就是为了替代前者。上述两者的使用场景相同,都是用在一些典型的模型中,如等待-通知模型生产-消费模型

  • 使用场景

当线程1执行过程中,需要【等待某个条件】才能继续执行,此时线程1会主动调用await()方法进入阻塞状态;然后线程2去完成这个条件,当条件完成后,线程2调用signal()方法,这时JVM会就会从阻塞线程中查找等待condition的线程1,线程1收到信号后,它的线程状态就会变成可执行状态。

  • 优势

wait/notify是Java底层级方法,通过与对象监视器完成线程合作,而Condition是JUC中的工具类,它是通过与Lock协作完成流程控制,具有很高的扩张性,同时增加很多特性,如支持不响应中断支持多种等待条件

二、Condition核心方法

Condition是一个接口,我们先看下其内部有哪些方法:

  • await: 当前线程进入等待,直到收到信号(signal)或者被中断;
  • await(long time, TimeUnit unit): 当前线程进入等待,直到收到信号或者被中断、到达指定等待时间(time);
  • awaitNanos(long nanosTimeout): 和上述方法一样,主要区别为时间单位不同,且当前方法返回值表示剩余超时时间。
  • awaitUninterruptibly(): 当前线程进入等待,直到收到信号,该方法不响应中断;
  • awaitUntil(Date deadline): 当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,返回false
  • signal(): 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁。
  • signalAll(): 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁。

三、Condition代码演示

  • 基本用法

首先我们看一下Condition的基本用法,线程调用await方法开始阻塞,调用signal方法唤醒等待中的线程。

public class ConditionDemo {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
​
    void method_1() throws InterruptedException {
        lock.lock();
        try{
            System.out.println("条件不满足,开始阻塞");
            condition.await();
            System.out.println("条件满足,继续执行");
        }finally {
            lock.unlock();
        }
    }
​
    void method_2() {
        lock.lock();
        try{
            System.out.println("执行完所需条件,开始唤醒等待的线程");
            condition.signal();
        }finally {
            lock.unlock();
        }
    }
​
    public static void main(String[] args) throws InterruptedException {
        ConditionDemo conditionDemo = new ConditionDemo();
        //线程2负责唤醒阻塞的线程
        new Thread(() -> {
            try {
                Thread.sleep(1000);
                conditionDemo.method_2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
​
        //线程1执行方法1
        conditionDemo.method_1();
    }
}

打印结果如下:

执行结果1.gif

可以看到,线程1执行到await()方法后会进行阻塞,直到线程2调用signal()方法,线程1才恢复执行。

有一点需要注意等待线程唤醒线程不能为同一个线程,例如将上面案例修改为下面这段,那程序会进入无休止的等待中。原因也非常简单,因为当线程调用完method_1时就已经陷入阻塞,不会再继续执行method_2了。

//修改代码如下,程序将陷入阻塞
conditionDemo.method_1();
conditionDemo.method_2();
  • Condition实现生产者消费者模式

前面我们用wait/notify实现过生产者消费者模式,今天我们看一下使用Condition是如何实现的,下面代码。

public class ConditionDemo2 {
​
    private int queueMaxSize = 5;
    private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueMaxSize);
    private Lock lock = new ReentrantLock();
    private Condition consConditon = lock.newCondition();
    private Condition prodConditon = lock.newCondition();
​
    public static void main(String[] args) {
        ConditionDemo2 conditionDemo2 = new ConditionDemo2();
        Producer producer = conditionDemo2.new Producer();
        Consumer consumer = conditionDemo2.new Consumer();
        producer.start();
        consumer.start();
    }
    //消费者线程
    class Consumer extends Thread {
        @Override
        public void run() {
            consume();
        }
        private void consume() {
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == 0) {
                        System.out.println("队列空,等待数据");
                        try {
                            prodConditon.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.poll();
                    consConditon.signalAll();
                    System.out.println("从队列里取走了一个数据,队列剩余" + queue.size() + "个元素");
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    //生产者线程
    class Producer extends Thread {
        @Override
        public void run() {
            produce();
        }
        private void produce() {
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == queueMaxSize) {
                        System.out.println("队列已满,等待消费者消费");
                        try {
                            consConditon.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.offer(1);
                    prodConditon.signalAll();
                    System.out.println("插入元素,队列当前数量" + queue.size());
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}

结果如下:

执行结果2.gif

分析一下上述代码的执行流程:

  • 生产者线程获取锁,当队列数量小于队列最大数量时,开始插入数据;
  • 插入完数据,生产者调用signalAll唤醒阻塞中的消费者线程;
  • 消费者线程尝试获取锁,如果成功获取,会消费队列中的数据,如果没有获取到,那生产者线程会继续插入数据到队列中,直至队列数量达到最大时,生产者会调用await进入阻塞;
  • 消费者消费完毕后,消费者会调用signalAll方法唤醒阻塞中的生产者线程,当生产者没有获取到锁,那消费者会持续消费,直到队列为空时,消费者会调用await进入阻塞。

四、Condition注意事项

  • Condition用于替代wait/notify来实现流程控制,在用法和性质上,两者基本一致。
  • await方法子自动释放持有的Lock锁,这点和Object.wait()一样,无需主动释放。
  • 调用await的时候,必须持有锁,否则会抛出异常,也是和Object.wait()一样。

总结

Condition的用法比较简单,结合之前介绍的wait/notify,相信小伙伴们也是很轻松就能掌握。最近在整理一些其他专栏,所以更新也比较慢,下一章预计更新最后一种流程控制类CyclicBarrier

点关注,防走丢

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花Gie ,有问题大家随时留言讨论 ,我们下期见🦮。

原创不易,你们的点赞是我坚持的动力,如果你觉得这篇文章对你有点用的话,感谢老铁为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!