一、等待唤醒方法概述
<典型案例>生产者消费者模型(简易版)
生产者:
1)循环交替生产“黄色的馒头”和“白色的包子”
2)把生产好的“馒头”或“包子”放入一个篮子里
3)当篮子中有”馒头“或”包子“时-->通知消费者来吃
消费者:
1)不断从篮子里面拿”包子“或”馒头“吃
2)当篮子没食物时-->通知生产者生产”包子“或”馒头“
要完成上面的功能,需要用到线程间的通讯技术(等待唤醒机制:多个线程间的一种协作机制)。
协作机制:
在一个线程进行了规定操作后, 就进入等待状态(wait())
等待其他线程执行完他们的指定代码过后 再将其唤醒(notify())
在有多个线程进行等待时, 如果需要,可以唤醒所有的等待线程notifyAll()
举例:wait/notify 就是线程间的一种协作机制。
方法:
1)wait():让调用方法的线程进入阻塞状态,并释放同步监视器(释放锁)。
2)notify():唤醒其中一个通过wait()方法进入阻塞的线程。
3)notifyAll():唤醒全部通过wait()方法进入阻塞的线程。
唤醒的意思就是让通过wait()方法进入阻塞的线程具备执行资格。必须注意的是,这些方法都是在同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。
示例:
package com.powernode.thread;
|
Q:为什么这些操作线程的方法定义在Object类中?
答:因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象,能被任意对象调用的方法一定是定义在Object类中。
wait()方法有三种形式:
1)无时间参数的wait()方法(一直等待,直到其他线程通知)
2)带毫秒参数的wait()方法
3)带毫秒、微秒参数的wait()方法(这两种方法都是等待指定时间后自动苏醒)。并且调用wait()方法的当前线程会释放对该同步监视器的锁定
使用wait、notify和notifyAll三个方法必须明白下面几点:
1)wait()、notify()和notifyAll()这三个方法都是java.lang.Object类提供的方法。
2)使用wait()方法进入等待状态的线程,还会释放掉锁,并且只有其它线程调用notify()或者notifyAll()方法,则进入wait()状态的锁才能被唤醒。
3)wait()、notify()和notifyAll()这三个方法,都必须在同步代码块或同步方法中,并且都必须通过“同步监视器”来调用,也就是唤醒当前“同步监视器”中正在wait()状态的锁,否则就会抛出IllegalMonitorStateException异常。
sleep()和 wait()方法的区别
1)sleep方法是Thread类的方法,而wait方法是Object类的方法。
2)sleep方法可以在任何地方使用,而wait方法必须在同步代码中使用。
3)sleep在休眠的时间内,不能唤醒,而wait在等待的时间内,能被唤醒。
4)sleep不释放同步锁,会一直持有锁,而wait方法会释放同步锁。
二、生产者消费者模型
所谓的生产者消费者问题,实际包含两类线程:
1)一类是生产者线程:负责生产数据;
2)一类是消费者线程:负责消费数据;
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像一个仓库。
生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;
消费者只需要从共享数据区中获取数据,并不需要生产者的行为;
学习“生产者消费者模式”,一定的明白什么是生产者,什么是消费者,什么是缓冲区。
什么是生产者?
生产者指的是负责生产数据的模块,此案例中对应的就是负责交替生产“黄色的馒头”和“白色的包子”的生产者类(ProductRunnable类)。
什么是消费者?
消费者指的是负责处理数据的模块,此案例中对应的就是消费“黄色的馒头”和“白色的包子”的消费者类(ConsumeRunnable类)。
什么是缓冲区?
消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。此案例中对应的就是盛放“黄色的馒头”和“白色的包子”的篮子,对应的类就是ProductStack类。
缓冲区是实现并发的核心,缓冲区的设置有3个好处 。
1)实现线程的并发协作。
有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需管生产者生产的情况。这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。
2)解耦了生产者和消费者。
生产者不需要和消费者直接打交道。
3)解决忙闲不均,提高效率。
生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。
生产者消费者模式的代码实现
一、商品类
1>商品类分析
要想完成生产者消费者模式的代码实现,首先我们应该创建一个商品类,商品类包含两个属性,分别为name属性和color属性,代表生产者循环交替生产的“黄色的馒头”和“白色的包子”。
2> 商品类的代码实现
// 商品类 public class Product { // 商品名称 private String name; // 商品颜色 private String color; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } } |
二、仓库类
1>仓库类分析
product属性:代表存储在仓库中商品(可能是“黄色馒头”,也有可能是“白色包子”)
flag属性:用于标记仓库中是否有商品存在。
仓库类中,本案例的核心:
1)生产商品方法(product)
2)消费商品方法(consume)。
生产商品实现步骤:
情况一:如果仓库中已经存在商品,那么生产者线程进入等待,不再进行商品的生产。
情况二:如果仓库中不存在商品,则生产者线程开始生产商品,生产商品完毕之后更改仓库商品状态。
最后:唤醒消费者线程,通知消费者线程可以消费产品了。
消费商品实现步骤:
情况一:如果仓库中不存在商品,那么消费者线程进入等待。
情况二:如果厂库中存在商品,则消费者线程开始消费商品,商品消费完毕之后更改仓库商品状态
最后:唤醒生成者线程,通知生产者线程可以继续生产商品了。
2> 仓库类的代码实现
// 仓库类 public class ProductStack { // 商品对象 private Product product; /** * 设置仓库是否有商品的标记 * 如果标记值为true,则证明仓库中有商品 * 如果标记值为false,则证明仓库中没有商品 */ private boolean flag; // 默认值为false // 构造方法 public ProductStack(Product product) { this.product = product; } // 生产商品方法 public synchronized void product(String name, String color) { // 1.如果有商品,则等待 if(flag) { try { this.wait(); // 该生产者线程进入线程池等待 } catch (InterruptedException e) { e.printStackTrace(); } } // 2.如果没有商品,则生产。 product.setName(name); try { // 线程等待:主要作用是为了切换线程 Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } product.setColor(color); System.out.println("生产者----->" + color + name); // 3.更改商品状态 this.flag = true; // 4.唤醒消费者线程,告诉消费者可以消费了 this.notify(); } // 消费商品方法 public synchronized void consume() { // 1.如果没有商品,则等待 if (!flag) { try { this.wait(); // 消费者线程进入线程池等待 } catch (InterruptedException e) { e.printStackTrace(); } } // 2.如果有商品,则消费 // 输出消费的商品信息 System.out.println("消费者-->" + product.getColor() + product.getName()); // 3.更改仓库商品状态 this.flag = false; // 4.唤醒生产着线程,告诉生产者可以继续生产了 this.notify(); } } |
三、生产者类&消费者类
1>生产者类&消费者类分析
生产者类:负责交替生产“黄色的馒头”和“白色的包子”
消费者类:负责消费商品,也就是输出商品信息。
生产者类生产商品时只需要调用product()方法即可,消费者类消费商品时也只需要调用consume()方法即可,
注:消费者和生产者操作的都是同一个仓库,也就是说消费者类和生产者类中的stack是一个共享对象,那么创建消费者和生产者对象时,就需要在构造方法中传入同一个仓库对象才行!
2>生产者类和消费者类的代码实现
// 生产者类 class ProductRunnable implements Runnable { int index = 0; // 商品 private ProductStack stack; public ProductRunnable(ProductStack stack) { this.stack = stack; } // 实现交替生产,奇数次生产:黄色馒头,偶数次生产:白色包子 @Override public void run() { while(true) { if(index%2 == 0) { // 生产白色的包子 stack.product("包子", "白色"); } else { // 生产黄色馒头 stack.product("馒头", "黄色"); } index++; } } } // 消费者类 class ConsumeRunnable implements Runnable { // 商品 private ProductStack stack; public ConsumeRunnable(ProductStack stack) { this.stack = stack; } @Override public void run() { while(true) { // 消费商品 stack.consume(); } } } |
四、测试类
1>测试类分析
最后,我们来编写测试类,在测试类的main方法中开启生产者线程和消费者线程,但是切记需要在生产者类和消费者类的构造方法中传入同一个仓库对象!
2>测试类代码实现
// 测试类 public class Test { public static void main(String[] args) { // 实例化一个商品类 ProductStack stack = new ProductStack(new Product()); // 实例化生产者和消费者任务类 ProductRunnable pr = new ProductRunnable(stack); ConsumeRunnable cr = new ConsumeRunnable(stack); // 开启线程 new Thread(pr).start(); new Thread(cr).start(); } } |
通过执行代码,我们可以明显看生产者交替的生产“黄色的馒头”和“白色的包子”,并且生产者每生产一个商品,消费者就对应消费对应生产的商品,实现生产者和消费者交替执行的功能。
三、多生产者多消费者问题
如果我们开启多个线程会怎么样呢?例如在测试类中,我们开启两个生产者线程和两个消费者线程,还能实现生产者和消费者交替执行的功能吗?
【示例】在测试类中开启两个生产者线程和两个消费者线程
// 测试类 public class Test { public static void main(String[] args) { // 实例化一个商品类 ProductStack stack = new ProductStack(new Product()); // 实例化生产者和消费者任务类 ProductRunnable pr = new ProductRunnable(stack); ConsumeRunnable cr = new ConsumeRunnable(stack); // 开启线程 Thread th1 = new Thread(pr); Thread th2 = new Thread(pr); // 新增的生产者线程 Thread th3 = new Thread(cr); Thread th4 = new Thread(cr); // 新增的消费者线程 th1.start(); th2.start(); th3.start(); th4.start(); } } |
运行以上案例代码,输出结果如下:
运行后发现,加上th2和th4之后结果就错了。
为什么两个线程的时候执行结果正确,然而四个线程的时候就不对了呢?接下来我们就基于以上程序运行结果来分析,假设th3线程刚执行完毕,然后执行notify唤醒在线程池中的任意一个线程,恰好唤醒了th4线程,th4线程接到唤醒通知后立即往下执行(也就从if判断中的this.wait()之后执行),而不会进行if条件判断,这样就造成了消费者消费了两次“白色包子”,也就是在控制台输出了两次“消费者-->白色包子”。
解决这个问题其实很简单,我们只需要修改仓库类(ProductStack类)中生产商品方法和消费商品方法即可,让线程被唤醒之后再次进行条件判断。解决方案就是把两个方法中的if变为while,让线程被唤醒并获得执行权之后再次进行条件判断,这样就能避免连续生产多个商品或连续消费多个商品的情况发生。
【示例】仓库类代码的优化实现
// 仓库类 public class ProductStack { // 商品对象,属于共享数据 private Product product; // 设置仓库是否有商品的标记 private boolean flag; // 默认值为false // 构造方法 public ProductStack(Product product) { this.product = product; } // 生产商品方法 public synchronized void product(String name, String color) { // 1.如果有商品,则等待 while(flag) { // 将if判断标记修改为while判断标记 try { this.wait(); // 该生产者线程进入线程池等待 } catch (InterruptedException e) { e.printStackTrace(); } } // 2.如果没有商品,则生产。 product.setName(name); try { // 线程等待,主要作用是为了切换线程 Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } product.setColor(color); System.out.println("生产者----->" + color + name); // 3.更改商品状态 this.flag = true; // 4.唤醒线程池中任意一个线程,未必是消费者线程 this.notify(); // 注意:此处代码未做任何修改,但是唤醒的未必是消费者线程 } // 消费商品方法 public synchronized void consume() { // 1.如果没有商品,则等待 while(!flag) { // 将if判断标记修改为while判断标记 try { this.wait(); // 消费者线程进入线程池等待 } catch (InterruptedException e) { e.printStackTrace(); } } // 2.如果有商品,则消费 // 输出消费的商品信息 System.out.println("消费者-->" + product.getColor() + product.getName()); // 3.更改商品状态 this.flag = false; // 4.唤醒线程池中任意一个线程,未必是生产者线程 this.notify(); // 注意:此处代码未做任何修改,但是唤醒的未必是生产者线程 } } |
修改完成之后,重新运行程序,我们发现程序进入了死循环,这又是怎么回事呢?
出现死循环的原因是:while判断标记+notify()导致的死锁。假设th2、th3、th4线程都进入了等待状态,那么能执行的只有th1线程。
如果此时仓库中的flag值为true,那么th1线程也直接进入等待状态,依旧造成了死锁。
如果此时仓库中的flag值为false,那么th1生产者线程可以正常的进行商品生产,商品生产完毕之后把仓库中的flag值为true,然后执行notify()方法,唤醒线程池中的任意一个线程,恰巧此次唤醒的就是th2线程,那么目前可执行的线程就只有th1和th2线程,并且仓库中的falg值为true,最终可执行th1和th2线程都会进入等待状态,也就造成了死锁。
解决方案:因为notify()方法只能唤醒一个线程,如果本方线程唤醒了本方线程,那就没有任何意义。此处我们需要notifyAll()方法来唤醒线程池中的所有线程,让所有等待的线程都唤醒避免了出现死锁的情况,也就是需要将两个方法中的this.notify()方法换为this.notifyAll()方法即可!
【示例】仓库类代码的优化实现
// 仓库类 public class ProductStack { // 商品对象,属于共享数据 private Product product; // 设置仓库是否有商品的标记 private boolean flag; // 默认值为false // 构造方法 public ProductStack(Product product) { this.product = product; } // 生产商品方法 public synchronized void product(String name, String color) { // 1.如果有商品,则等待 while(flag) { try { this.wait(); // 该生产者线程进入线程池等待 } catch (InterruptedException e) { e.printStackTrace(); } } // 2.如果没有商品,则生产。 product.setName(name); try { // 线程等待,主要作用是为了切换线程 Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } product.setColor(color); System.out.println("生产者----->" + color + name); // 3.更改商品状态 this.flag = true; // 4.唤醒线程池中所有的线程 this.notifyAll(); } // 消费商品方法 public synchronized void consume() { // 1.如果没有商品,则等待 while(!flag) { try { this.wait(); // 消费者线程进入线程池等待 } catch (InterruptedException e) { e.printStackTrace(); } } // 2.如果有商品,则消费 // 输出消费的商品信息 System.out.println("消费者-->" + product.getColor() + product.getName()); // 3.更改商品状态 this.flag = false; // 4.唤醒线程池中所有的线程 this.notifyAll(); } } |
通过以上两种优化方案,我们就实现了多生产者多消费者模式。生产者每生产一个商品,消费者就对应消费对应生产的商品,也就是完成了生产者和消费者交替执行的功能。