大家好,上篇文章为大家介绍了线程间通信和协作的一些基本方式,那这篇文章就来介绍一下经典的wait-notify机制吧。
什么是wait-notify机制?
想象一下有两个线程A、B,如果业务场景中需要这两个线程交替执行任务(比如A执行完一次任务后换B执行,B执行完后再换A执行这样重复交替),之前的基本通信方式只能让线程暂停一段指定时间,Join方法也无法做到这种交替执行的要求,那怎么办呢?
别急,针对这种场景java同样为我们提供了一种经典的线程通信方式——wait-notify机制,这里涉及到下面的三个方法(关于锁的知识后文会详细讲):
wait方法:当前线程在调用wait方法会先让出当前线程持有的对象锁以便让其他线程能够获取,然后当前线程会停止执行并进入WAITING状态。直到接收到唤醒或中断信号后,当前线程才会继续尝试获取对象锁。如果此时获取对象锁成功,就能继续执行任务。
notify方法:当前线程的任务即将执行完毕并发出唤醒信号,此时只有接收到唤醒信号的线程才会尝试获取对象锁。当然此时可能获取对象锁会失败,因为notify方法不会即时释放锁,而是需要等到线程执行完毕后才会真正释放锁。
notifyAll方法:和notify方法作用相似,唯一不同的就是该方法会对当前所有在等待这个对象锁的线程发出唤醒信号。至于最终是哪个线程抢到了对象锁,就要看哪个线程比较“幸运”啦。
关于这几个方法,还有以下两点需要关注:
1.wait、notify、notifyAll这三个方法都是Object类中定义的,而Object类是所有类的父类,所以在java中的所有对象都会继承这三个方法。
2.这三个方法必须在同步块中被调用(之后会介绍同步块),如果在同步块之外调用这三个方法,java会抛出java.lang.IllegalMonitorStateException这个异常。
基于wait-notify机制的单生产者-单消费者模型
上面已经介绍了wait-notify机制用到的方法以及需要注意的点,实际上针对这个机制,有一个非常著名、非常经典的模型——生产者消费者模型。
什么是生产者-消费者模型呢?简单来说就是这么个场景:有两种线程分别是生产者线程和消费者线程,还有一个固定大小的资源队列。
生产者的任务是根据原料生产出产品,并将生产好的产品往队列里扔;消费者的任务呢就是从队列里面拿已经生产好的产品去进行包装。
我们可以看到在这个场景中,因为队列可容纳的资源是有限的,所以当队列满时,生产者就没办法继续往队列里放产品,此时生产者就需要等待消费者从队列里拿走产品后,才能继续往队列里放产品;
而消费者也是一样,当队列为空时,消费者就无法从队列里拿到产品,此时就需要等待生产者成功生产出产品并往队列里扔,才能继续从队列里拿产品。
这个场景是wait-notify机制最适合发挥作用的场景,下面是一个单生产者-单消费者的模拟代码:
1 /** 2 * 基于wait-notify机制的单生产者-消费者模型 3 */ 4 public class ProducerAndConsumer { 5 6 public static void main(String[] args) { 7 Resource resource = new Resource(); 8 //生产者线程 9 ProducerThread p1 = new ProducerThread(resource); 10 //消费者线程 11 ConsumerThread c1 = new ConsumerThread(resource); 12 13 p1.start(); 14 c1.start(); 15 16 } 17 } 18 19 20 /** 21 * 公共资源类 22 * @author 23 * 24 */ 25 class Resource{//重要 26 //当前资源数量 27 private int num = 0; 28 //资源池中允许存放的资源数目 29 private int size = 10; 30 31 /** 32 * 从资源池中取走资源 33 */ 34 public synchronized void remove(){ 35 if(num > 0){ 36 num--; 37 System.out.println("消费者" + Thread.currentThread().getName() + 38 "消耗一件资源," + "当前线程池有" + num + "个"); 39 notifyAll();//通知生产者生产资源 40 }else{ 41 try { 42 //如果没有资源,则消费者进入等待状态 43 wait(); 44 System.out.println("消费者" + Thread.currentThread().getName() + "线程进入等待状态"); 45 } catch (InterruptedException e) { 46 e.printStackTrace(); 47 } 48 } 49 } 50 /** 51 * 向资源池中添加资源 52 */ 53 public synchronized void add(){ 54 if(num < size){ 55 num++; 56 System.out.println("生产者" + Thread.currentThread().getName() + "生产一件资源,当前资源池有" 57 + num + "个"); 58 //通知等待的消费者 59 notifyAll(); 60 }else{ 61 //如果当前资源池中有10件资源 62 try{ 63 wait();//生产者进入等待状态,并释放锁 64 System.out.println(Thread.currentThread().getName()+"线程进入等待"); 65 }catch(InterruptedException e){ 66 e.printStackTrace(); 67 } 68 } 69 } 70 } 71 72 73 /** 74 * 消费者线程 75 */ 76 class ConsumerThread extends Thread{ 77 private Resource resource; 78 public ConsumerThread(Resource resource){ 79 this.resource = resource; 80 } 81 @Override 82 public void run() { 83 while(true){ 84 try { 85 Thread.sleep(1000); 86 } catch (InterruptedException e) { 87 e.printStackTrace(); 88 } 89 resource.remove(); 90 } 91 } 92 } 93 94 95 /** 96 * 生产者线程 97 */ 98 class ProducerThread extends Thread{ 99 private Resource resource; 100 public ProducerThread(Resource resource){ 101 this.resource = resource; 102 } 103 @Override 104 public void run() { 105 //不断地生产资源 106 while(true){ 107 try { 108 Thread.sleep(1000); 109 } catch (InterruptedException e) { 110 e.printStackTrace(); 111 } 112 resource.add(); 113 } 114 } 115 116 }
童鞋们可以运行代码试试,这里资源池最大允许放10个产品。
这里留一个问题给大家思考,如果我这里的add和remove方法不加synchronized修饰,就会抛出java.lang.IllegalMonitorStateException异常,那么是什么原因导致java必须要这么做呢?我会在介绍synchronized关键字的时候公布答案。
好了,wait-notify机制到这里就介绍完毕,希望大家能够理解。下篇文章会为大家讲解一下volatile这个关键字的用法。