等待唤醒机制
线程间通信
- 多个线程处理同一个资源,但是处理的动作(线程的任务)却不同
- 就是上一篇文章的例子 Java 多线程之线程状态-3里面列举的生产者跟消费者的Demo
!!!为什么要处理线程间通信!!!
- 多个线程并发执行时,默认情况CPU随机切换线程的,当我们需要多个线程来完成同一个任务
- 我们希望他们有规律执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据
!!!如何保证线程间通信有效利用资源!!!
- 多个线程再处理同一资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一变量的使用或操作
- 就是多个线程再操作同一数据时,避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程能有效利用资源,而这种手段叫做等待唤醒机制
等待唤醒机制
多个线程之间一种协作机制,谈到线程我们经常小岛的时线程间的竞争(race),比如争夺锁,但这并不是故事的全部,线程间也会有协作机制
就好比公司里你和你的同时,可能存再晋升的竞争,但更多的时候你们更多的是一起合作完成任务
就是一个线程进行了规定操作后,就进入等待状态wait(),等待其他线程执行完他们的指定代码过后,再将其唤醒notify()
再有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有等待线程
wait/notify 就是线程间的一种协作机制
等待唤醒中的方法
等待唤醒机制就是用于解决线程间通信的问题的,使用3个方法的含义如下
- wait:线程不在活动,不在参与调度,进入 wait set中,因此不会浪费CPU资源,也不会竞争锁,这是线程状态时WAITING,它还要等着别的线程执行一个特别的动作,也就是通知( notify ) 再这个对象上等待的线程wait set 中释放出来,重新进入调度队列( ready queue)中
- notify: 则选取所通知的对象wait set 中的一个线程释放;例如,参观有空位置后,等候就餐最久的顾客最先入座
- notifyAll:释放所通知对象的wait set上全部的线程
注意: 哪怕只通知了一个等待线程,被通知的线程也不能立刻恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不在持有锁,所以它需要再次尝试获取锁(可能面临其他线程的竞争),成功后才能再当初调用wait方法之后恢复执行
总结如下:
- 如果获取锁,线程就从WAITING状态变成RUNNABLE阶段
- 否则,从wait set出来,又进入entry set
调用wait和notify方法要注意的细节?
- wait方法与notify方法必须要由同一锁对象调用,因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
- wait方法与notify方法时属于Object类中的方法,因为:锁对象可以时任意对象,而任意对象的所属类都是继承了Object类的
- wait方法和notify方法必须要再同步代码块或时同步函数中使用,因为:必须要通过锁对象调用2个方法
生产者和消费者的问题
- 等待唤醒机制其实就是经典生产者和消费者的问题
- 就拿生产包子消费包子来说等待唤醒机制如何有效利用资源
!!!上代码!!!
// 包子类
public class BaoZi{
// 皮
String pi;
// 馅
String xian;
boolean isHave = false;
}
// 包子店类
public class BaoZiPu implements Runnable{
private BaoZi baoZi;
public BaoZiPu(BaoZi baoZi){
this.baoZi = baoZi;
}
@Override
public void run(){
int count = 0;
while(true){
synchronized(baoZi){
// 如果存在包子 包子铺线程等待
if(baoZi.isHave == true){
baoZi.wait();
}
if (count % 2==0){
baoZi.pi = "薄皮";
baoZi.xian = "三鲜馅";
}else{
baoZi.pi = "冰皮";
baoZi.xian = "牛肉大葱馅";
}
System.out.println("包子铺正在生产:"+baoZi.pi+baoZi.xian+"的包子");
count++;
// 三秒制作
Thread.sleep(3000);
System.out.println("包子铺生产好了:"+baoZi.pi+baoZi.xian+"的包子");
// 设置包子状态
baoZi.isHave = true;
// 唤醒吃货线程
baoZi.notify();
}
}
}
}
// 包子店类
public class ChiHuo implements Runnable{
private BaoZi baoZi;
public ChiHuo(BaoZi baoZi){
this.baoZi = baoZi;
}
@Override
public void run(){
while(true){
synchronized(baoZi){
// 如果不存在包子 吃货线程等待
if(baoZi.isHave == false){
baoZi.wait();
}
System.out.println("吃货正在吃:"+baoZi.pi+baoZi.xian+"的包子");
System.out.println("吃货已经把:"+baoZi.pi+baoZi.xian+"的包子吃完了,包子铺开始生产包子");
// 设置包子状态
baoZi.isHave = false;
// 唤醒包子铺线程
baoZi.notify();
}
}
}
}
public class EatBaoZiTest{
public static void main(String[] args){
BaoZi baoZi = new BaoZi();
new BaoZiPu(baoZi).start();
new ChiHuo(baoZi).start();
}
}
!!!结果!!!
因为是无线循环所以,我们的预期是一样的!!!,这就是一个简单的生产者和消费者的案列
线程池概念
线程池思想概述
我们使用线程的时候就去创建一个线程,实现起来非常简单,但是会有一个问题:
如果并发线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统效率,因为频繁创建线程和销毁线程是需要时间
有没有一种办法使得线程可以复用,执行完一个任务,不被销毁,可以继续执行其他任务呢?
线程池概念
容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程的操作,无需反复创建小城而消耗过多的资源
合理利用线程池的三个好处:
- 降低资源消耗,减少了创建和销毁线程的次数,每个工作栈都可以被重复利用,可执行多个任务
- 提高响应速度,当任务到达时,任务可以不需要等到线程就立刻执行
- 提高线程可管理性,可以根据系统的承受能力,调整线程池工作栈线程的数目,防止消耗过多的内存,而导致服务器宕机(每个线程需要大约1MB内存,线程开的越多,消耗的内存越大)
线程池的使用
Java里面线程池的顶级接口 java.util.concurrent.Executor 严格意义上Executor并不是线程池,而只是一个执行线程的工具
真正的线程池接口java.util.concurrent.ExecutorService
注意:配置一个线程池比较复杂,尤其对于线程池原理不是很清楚的情况下,很有可能配置的线程池不是较优,因此Executors线程工厂类提供了一些静态工厂,生成一些常用的线程池, 官方建议用Executors工具类创建线程池对象
- public static ExectorSercvice newFixedThreadPool(int nThreads); 返回线程池对象(指定线程池数目)
获取到了ExecutorService对象如何使用呢?
- public Future<?> submit(Runnable task); 获取线程池中的某一线程的对象,并执行
Future接口:用来记录线程任务执行完毕产生的结果,线程池创建与使用
使用线程池线程对象的步骤:
- 创建线程池对象
- 创建Runnable 接口子类对象
- 提交Runnable 接口子类对象
- 关闭线程池(一般不做)
public class EatBaoZiTest{
public static void main(String[] args){
BaoZi baoZi = new BaoZi();
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(new BaoZiPu(baoZi));
executorService.submit(new ChiHuo(baoZi));
}
}