第一章 等待唤醒机制
1.1 线程间通信
- 概念:
- 多个线程在处理同一个资源(包子),但是处理的动作(线程的任务)却不相同。
- 比如:
- 线程A用来生产包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题
1.2 等待唤醒机制
- 等待与唤醒机制:线程之间的通信
- 【重点】:有效的利用资源[包子](生产一个包子,吃一个包子,再生产一个包子,再吃一个包子)
- 通信:对包子的状态进行判断
* ==没有==包子-->吃货线程唤醒包子铺线程-->吃货线程等待-->包子铺线程做包子-->做好包子-->修改包子的状态==有==
* ==有==包子-->包子铺线程唤醒吃货线程-->包子铺线程等待-->吃货线程吃包子-->修改包子的状态==没有==
* ==没有==包子-->吃货线程唤醒包子铺线程-->吃货线程等待-->包子铺线程做包子-->做好包子-->修改包子的状态==有==
* ...
- 等待唤醒中的方法
- void wait()
- 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
- void notify()
- 唤醒在此对象监视器上等待的单个线程。
- 注:会继续执行wait方法之后的代码
- void notifyAll()
- 唤醒在此对象监视器上等待的所有线程。
- 注意事项:
- 哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步代码块内,而此刻它已经不持有锁,所以它需要再次去获取锁(++很有可能面临其他线程的竞争++),成功后才能在当初调用wait方法之后的地方恢复执行。
- 总结:
- 如果能获取锁,线程就从WAITING状态变成RUNNABLE状态
- 否则,从wait set出来,又进入entry set,线程又从WAITING状态变成BOLCKED状态
- 调用wait和notify方法需要注意的细节
- wait和notify必须要由同一个锁对象调用
- 对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
- wait与notify是属于Object方法的
- 锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的
- wait和notify必须要在同步代码块或者是同步方法(同步函数)中使用
- 必须要通过锁对象调用这2个方法
1.3 等待唤醒机制案例分析
- 等待唤醒机制其实就是经典的“生产者与消费者”问题
- 1.1中包子案例分析:
1.4 等待唤醒机制代码实现
- 包子铺类的注意事项:
- 包子铺线程与吃货线程关系–>通信(互斥)
- 必须使用同步技术两个线程只能有一个在执行
- 锁对象必须保证唯一,可以使用包子对象作为锁对象
- 包子铺这个类和吃货这个类把包子对象作为参数传入进来
- 需要在成员位置创建一个变量
- 使用带参构造方法,为这个包子变量赋值
//定义一个包子类:BaoZi.java
/*
包子类
属性:
皮
馅
包子的有无状态:true,false
*/
public class BaoZi {
//皮
String pi;
//馅
String xian;
//包子的有无状态:true,false,初始为false
boolean flag = false;
}
//定义一个包子铺类来继承Thread:BaoZiPu.java
public class BaoZiPu extends Thread{
//1. 需要在成员位置创建一个变量
private BaoZi bz;
//2. 使用带参构造方法,为这个包子变量赋值
public BaoZiPu(BaoZi bz) {
this.bz = bz;
}
//重写run方法:生产包子
@Override
public void run() {
//定义一个变量(用来判断生产那种包子)
int count = 0;
//while让包子铺一直生产包子
while (true){
//同步技术,保证两个线程只有一个在执行
synchronized(bz){ //同步代码块的锁对象就用包子对象
//对包子状态进行判断
if(bz.flag == true){
//调用wait方法进入等待状态
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒之后执行,包子铺生产包子
//交替生产两种包子
if(count % 2 == 0){
//生产 薄皮 三鲜馅包子
bz.pi = "薄皮";
bz.xian = "三鲜馅";
}else {
//生产 冰皮 牛肉大葱馅包子
bz.pi = "冰皮";
bz.xian = "牛肉大葱馅";
}
count++;
System.out.println("包子铺正在生产" + bz.pi + bz.xian + "的包子");
//生产包子需要3秒钟
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//包子铺生产好包子后,将包子状态修改为有
bz.flag = true;
//唤醒吃货线程,让吃货线程吃包子
bz.notify();
System.out.println("包子铺已经生产好了:" + bz.pi + bz.xian + "的包子,吃货可以开始吃了");
}
}
}
}
//定义一个吃货类来继承Thread:ChiHuo.java
public class ChiHuo extends Thread{
//1. 需要在成员位置创建一个变量
private BaoZi bz;
//2. 使用带参构造方法,为这个包子变量赋值
public ChiHuo(BaoZi bz) {
this.bz = bz;
}
//重写线程任务run:吃包子
@Override
public void run() {
//while让吃货一直吃包子
while (true){
//同步技术,保证两个线程只有一个在执行
synchronized(bz) { //同步代码块的锁对象就用包子对象
//对包子的状态进行判断
if(bz.flag == false){
//吃货调用wait方法进入等待状态
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒之后执行的代码,吃包子
System.out.println("吃货正在吃" + bz.pi + bz.xian + "的包子");
//吃货吃完包子后,将包子状态修改为无
bz.flag = false;
//唤醒包子铺线程,让包子铺生产包子
bz.notify();
System.out.println("吃货已经把:" + bz.pi + bz.xian + "的包子吃完了,包子铺开始生产包子");
System.out.println("=======================");
}
}
}
}
//主方法(测试类):Demo.java
public static void main(String[] args) {
//创建包子对象
BaoZi bz = new BaoZi();
//创建包子铺线程,开启,生产包子
new BaoZiPu(bz).start();
//创建吃货线程,开启,吃包子
new ChiHuo(bz).start();
}
//结果:
包子铺正在生产薄皮三鲜馅的包子
包子铺已经生产好了:薄皮三鲜馅的包子,吃货可以开始吃了
吃货正在吃薄皮三鲜馅的包子
吃货已经把:薄皮三鲜馅的包子吃完了,包子铺开始生产包子
=======================
包子铺正在生产冰皮牛肉大葱馅的包子
包子铺已经生产好了:冰皮牛肉大葱馅的包子,吃货可以开始吃了
吃货正在吃冰皮牛肉大葱馅的包子
吃货已经把:冰皮牛肉大葱馅的包子吃完了,包子铺开始生产包子
=======================
包子铺正在生产薄皮三鲜馅的包子
...
第二章 线程池
2.1 线程池的概念和原理
- 线程池的思想概念
2.2 线程池的概念和原理
- 概念:
- 线程池:其实就是一个容纳多个线程的容器【!】,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多的资源。
- 线程池的底层原理:
- 当程序第一次启动的时候,创建多个线程,保存到一个集合中
- 当我们想要使用线程的时候,就可以从集合中取出来线程使用
- Thread t = list.remove(0); //返回的是被移除的元素,(线程只能被一个任务使用)
- Thread t = linked.removeFirst(0);
- 当我们使用完线程,需要把线程归还给线程池
- list.add(t);
- linked.addLast(t);
- 常用LinkedList集合
- 注:JDK1.5之后,内置了线程池,可以直接使用,无需自己写2.3会讲【重点】
- 线程池工作原理:
- 注:如果有5个任务,3个线程,如果线程池中无空闲线程时
- 任务等待执行
- 等待其他某个任务执行完毕后,归还线程到线程池
- 再从线程池中获取线程,执行任务
- 线程池的优点:
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性