55 Condition与Object.wait/notify的关系:条件变量深度解析

图片[1]-55 Condition与Object.wait/notify的关系:条件变量深度解析 - 速优课-速优课

Condition与Object.wait/notify的关系:条件变量深度解析

📌 本文导读

本文将围绕「Condition、object.wait() 和 notify() 的关系?」这一主题展开深入探讨。

在Java并发编程的学习过程中,这是一个非常核心且高频的知识点。通过本文的学习,你将能够:

✅ 理解核心概念与原理 ✅ 掌握实际应用场景 ✅ 避开常见的使用陷阱 ✅ 应对面试中的相关问题


本课时我们主要介绍 Condition、Object 的 wait() 和 notify() 的关系。

下面先讲一下 Condition 这个接口,来看看它的作用、如何使用,以及需要注意的点有哪些。

Condition接口

作用

我们假设线程 1 需要等待某些条件满足后,才能继续运行,这个条件会根据业务场景不同,有不同的可能性,比如等待某个时间点到达或者等待某些任务处理完毕。在这种情况下,我们就可以执行 Condition 的 await 方法,一旦执行了该方法,这个线程就会进入 WAITING 状态。

通常会有另外一个线程,我们把它称作线程 2,它去达成对应的条件,直到这个条件达成之后,那么,线程 2 调用 Condition 的 signal 方法 [或 signalAll 方法],代表“这个条件已经达成了,之前等待这个条件的线程现在可以苏醒了”。这个时候,JVM 就会找到等待该 Condition 的线程,并予以唤醒,根据调用的是 signal 方法或 signalAll 方法,会唤醒 1 个或所有的线程。于是,线程 1 在此时就会被唤醒,然后它的线程状态又会回到 Runnable 可执行状态。

代码案例

我们用一个代码来说明这个问题,如下所示:

​
public class ConditionDemo {
​
    private ReentrantLock lock = new ReentrantLock();
​
    private Condition condition = lock.newCondition();
​
    void method1() throws InterruptedException {
​
        lock.lock();
​
        try{
​
            System.out.println(Thread.currentThread().getName()+":条件不满足,开始await");
​
            condition.await();
​
            System.out.println(Thread.currentThread().getName()+":条件满足了,开始执行后续的任务");
​
        }finally {
​
            lock.unlock();
​
        }
​
    }
​
    void method2() throws InterruptedException {
​
        lock.lock();
​
        try{
​
            System.out.println(Thread.currentThread().getName()+":需要5秒钟的准备时间");
​
            Thread.sleep(5000);
​
            System.out.println(Thread.currentThread().getName()+":准备工作完成,唤醒其他的线程");
​
            condition.signal();
​
        }finally {
​
            lock.unlock();
​
        }
​
    }
​
    public static void main(String[] args) throws InterruptedException {
​
        ConditionDemo conditionDemo = new ConditionDemo();
​
        new Thread(new Runnable() {
​
            @Override
​
            public void run() {
​
                try {
​
                    conditionDemo.method2();
​
                } catch (InterruptedException e) {
​
                    e.printStackTrace();
​
                }
​
            }
​
        }).start();
​
        conditionDemo.method1();
​
    }
​
}
​

在这个代码中,有以下三个方法。

  • method1,它代表主线程将要执行的内容,首先获取到锁,打印出“条件不满足,开始 await”,然后调用 condition.await() 方法,直到条件满足之后,则代表这个语句可以继续向下执行了,于是打印出“条件满足了,开始执行后续的任务”,最后会在 finally 中解锁。
  • method2,它同样也需要先获得锁,然后打印出“需要 5 秒钟的准备时间”,接着用 sleep 来模拟准备时间;在时间到了之后,则打印出“准备工作完成”,最后调用 condition.signal() 方法,把之前已经等待的线程唤醒。
  • main 方法,它的主要作用是执行上面这两个方法,它先去实例化我们这个类,然后再用子线程去调用这个类的 method2 方法,接着用主线程去调用 method1 方法。

最终这个代码程序运行结果如下所示:

​
main:条件不满足,开始 await
​
Thread-0:需要 5 秒钟的准备时间
​
Thread-0:准备工作完成,唤醒其他的线程
​
main:条件满足了,开始执行后续的任务
​

同时也可以看到,打印这行语句它所运行的线程,第一行语句和第四行语句打印的是在 main 线程中,也就是在主线程中去打印的,而第二、第三行是在子线程中打印的。这个代码就模拟了我们前面所描述的场景。

注意点

下面我们来看一下,在使用 Condition 的时候有哪些注意点。

  • 线程 2 解锁后,线程 1 才能获得锁并继续执行

线程 2 对应刚才代码中的子线程,而线程 1 对应主线程。这里需要额外注意,并不是说子线程调用了 signal 之后,主线程就可以立刻被唤醒去执行下面的代码了,而是说在调用了 signal 之后,还需要等待子线程完全退出这个锁,即执行 unlock 之后,这个主线程才有可能去获取到这把锁,并且当获取锁成功之后才能继续执行后面的任务。刚被唤醒的时候主线程还没有拿到锁,是没有办法继续往下执行的。

  • signalAll() 和 signal() 区别

signalAll() 会唤醒所有正在等待的线程,而 signal() 只会唤醒一个线程。

用 Condition 和 wait/notify 实现简易版阻塞队列

在第 05 讲,讲过如何用 Condition 和 wait/notify 来实现生产者/消费者模式,其中的精髓就在于用 Condition 和 wait/notify 来实现简易版阻塞队列,我们来分别回顾一下这两段代码。

用 Condition 实现简易版阻塞队列

代码如下所示:

​
public class MyBlockingQueueForCondition {
​
   private Queue queue;
​
   private int max = 16;
​
   private ReentrantLock lock = new ReentrantLock();
​
   private Condition notEmpty = lock.newCondition();
​
   private Condition notFull = lock.newCondition();
​
   public MyBlockingQueueForCondition(int size) {
​
       this.max = size;
​
       queue = new LinkedList();
​
   }
​
   public void put(Object o) throws InterruptedException {
​
       lock.lock();
​
       try {
​
           while (queue.size() == max) {
​
               notFull.await();
​
           }
​
           queue.add(o);
​
           notEmpty.signalAll();
​
       } finally {
​
           lock.unlock();
​
       }
​
   }
​
   public Object take() throws InterruptedException {
​
       lock.lock();
​
       try {
​
           while (queue.size() == 0) {
​
               notEmpty.await();
​
           }
​
           Object item = queue.remove();
​
           notFull.signalAll();
​
           return item;
​
       } finally {
​
           lock.unlock();
​
       }
​
   }
​
}
​

在上面的代码中,首先定义了一个队列变量 queue,其最大容量是 16;然后定义了一个 ReentrantLock 类型的 Lock 锁,并在 Lock 锁的基础上创建了两个 Condition,一个是 notEmpty,另一个是 notFull,分别代表队列没有空和没有满的条件;最后,声明了 put 和 take 这两个核心方法。

用 wait/notify 实现简易版阻塞队列

我们再来看看如何使用 wait/notify 来实现简易版阻塞队列,代码如下:

​
class MyBlockingQueueForWaitNotify {
​
   private int maxSize;
​
   private LinkedList<Object> storage;
​
   public MyBlockingQueueForWaitNotify (int size) {
​
       this.maxSize = size;
​
       storage = new LinkedList<>();
​
   }
​
   public synchronized void put() throws InterruptedException {
​
       while (storage.size() == maxSize) {
​
           this.wait();
​
       }
​
       storage.add(new Object());
​
       this.notifyAll();
​
   }
​
   public synchronized void take() throws InterruptedException {
​
       while (storage.size() == 0) {
​
           this.wait();
​
       }
​
       System.out.println(storage.remove());
​
       this.notifyAll();
​
   }
​
}
​

如代码所示,最主要的部分仍是 put 与 take 方法。我们先来看 put 方法,该方法被 synchronized 保护,while 检查 List 是否已满,如果不满就往里面放入数据,并通过 notifyAll() 唤醒其他线程。同样,take 方法也被 synchronized 修饰,while 检查 List 是否为空,如果不为空则获取数据并唤醒其他线程。

在第 05 讲,有对这两段代码的详细讲解,遗忘的小伙伴可以到前面复习一下。

Condition 和 wait/notify的关系

对比上面两种实现方式的 put 方法,会发现非常类似,此时让我们把这两段代码同时列在屏幕中,然后进行对比:

左:

​
public void put(Object o) throws InterruptedException {
​
   lock.lock();
​
   try {
​
      while (queue.size() == max) {
​
         condition1.await();
​
      }
​
      queue.add(o);
​
      condition2.signalAll();
​
   } finally {
​
      lock.unlock();
​
   }
​
}
​

右:

​
public synchronized void put() throws InterruptedException {
​
   while (storage.size() == maxSize) {
​
      this.wait();
​
   }
​
   storage.add(new Object());
​
   this.notifyAll();
​
}
​

可以看出,左侧是 Condition 的实现,右侧是 wait/notify 的实现:

​
lock.lock() 对应进入 synchronized 方法
​
condition.await() 对应 object.wait()
​
condition.signalAll() 对应 object.notifyAll()
​
lock.unlock() 对应退出 synchronized 方法
​

实际上,如果说 Lock 是用来代替 synchronized 的,那么 Condition 就是用来代替相对应的 Object 的 wait/notify/notifyAll,所以在用法和性质上几乎都一样。

Condition 把 Object 的 wait/notify/notifyAll 转化为了一种相应的对象,其实现的效果基本一样,但是把更复杂的用法,变成了更直观可控的对象方法,是一种升级。

await 方法会自动释放持有的 Lock 锁,和 Object 的 wait 一样,不需要自己手动释放锁。

另外,调用 await 的时候必须持有锁,否则会抛出异常,这一点和 Object 的 wait 一样。

总结

首先介绍了 Condition 接口的作用,并给出了基本用法;然后讲解了它的几个注意点,复习了之前 Condition 和 wait/notify 实现简易版阻塞队列的代码,并且对这两种方法,不同的实现进行了对比;最后分析了它们之间的关系。


🎯 总结与思考

核心要点回顾

通过本文的学习,我们深入探讨了「Condition、object.wait() 和 notify() 的关系?」相关的知识。主要内容包括:

  1. 基础概念:从底层原理出发,理解核心概念的本质
  2. 实现机制:分析源码层面的实现细节
  3. 应用场景:了解在实际项目中如何正确使用
  4. 注意事项:掌握常见的坑点与避坑方法

知识延伸

并发编程是Java高级开发必须掌握的核心技能。本文涉及的知识点不仅在日常开发中经常用到,也是各大互联网公司面试的高频考点。建议大家:

  • 🔍 结合源码深入理解原理
  • 🛠️ 多动手实践,在项目中应用
  • 📚 与其他并发知识点串联学习,形成知识体系
  • 💡 多思考为什么这么设计,而不只是怎么用

思考问题

  1. 你在实际项目中遇到过哪些与本文相关的问题?
  2. 还有哪些场景可以应用本文学到的知识?
  3. 本文讲解的内容还有哪些可以拓展的方向?

💡 欢迎在评论区留言讨论,一起交流进步!

© 版权声明
THE END
喜欢就支持一下吧
点赞13
相关推荐
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!