6、Condition和Object 的 wait() /notify() 的关系是什么?

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

1、Condition的作用

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

1.1 作用

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

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

1.2 代码案例

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

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();
        
        // 创建新线程调用method2
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    conditionDemo.method2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        // 主线程调用method1
        conditionDemo.method1();
    }
}

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

  • method1,它代表主线程将要执行的内容,首先获取到锁,打印出“条件不满足,开始 await”,然后调用 condition.await() 方法,直到条件满足之后,则代表这个语句可以继续向下执行了,于是打印出“条件满足了,开始执行后续的任务”,最后会在 finally 中解锁。

  • method2,它同样也需要先获得锁,然后打印出“需要 5 秒钟的准备时间”,接着用 sleep 来模拟准备时间;在时间到了之后,则打印出“准备工作完成”,最后调用 condition.signal() 方法,把之前已经等待的线程唤醒。

  • main 方法,它的主要作用是执行上面这两个方法,它先去实例化我们这个类,然后再用子线程去调用这个类的 method2 方法,接着用主线程去调用 method1 方法。

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

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

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

1.3 注意点

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

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

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

1.4 signalAll() 和 signal() 区别

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

前面一节我们已经讲过用 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 实现简易版阻塞队列,代码如下:

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 是否为空,如果不为空则获取数据并唤醒其他线程。

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

1.5 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 一样需要锁。

2、总结

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值