等待唤醒机制(生产者和消费者)
说明
之前的多线程是谁抢到CPU的执行权谁执行,而等待唤醒机制作为一种经典的多线程协作模式,可以实现线程的交替执行。
成员
实现等待唤醒机制需要三个成员:生产者、消费者、标志位
可以分别看作厨师、吃货、桌子
生产者(厨师)需要做的事情:
1.判断桌子上是否有食物
2.有:等待
3.没有:制作食物
4.把食物放在桌子上(修改食物状态为有食物)
5.叫醒等待的消费者开吃
消费者(吃货)需要做的事情:
1.判断桌子上是否有食物
2.没有:等待
3.有:开吃
4.吃食物(修改食物状态为没有食物)5.叫醒等待的生产者开吃继续做
标志位(桌子)需要做的事情:
标志是否有食物
常见方法
方法名称 | 说明 |
void wait() | 当前线程等待,直到被其他线程唤醒(底层会退出 synchronized 块,让其他线程可以获取锁) |
void notify( ) | 随机唤醒单个线程 |
void notifyAll() | 唤醒所有线程 |
代码演示
标志类
public class Table {//标志位,0表示桌子上没有食物,1表示有public static int flag = 0;//表示线程交替执行5次public static int count = 5;//表示锁对象public static Object lock = new Object();}
生产者类
public class Cook extends Thread {@Overridepublic void run() {//循环while (true) {//同步代码块synchronized (Table.lock) {//判断是否执行到了末尾if (Table.count == 0) {return;} else {//1.判断桌子上是否有食物if (Table.flag == 1) {//2.有:等待try {//用锁对象调用等待方法,底层会将线程与锁绑定//这样后续用锁对象调用唤醒方法,就会把与锁对象绑定的所有线程唤醒Table.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}} else {//3.没有:制作食物System.out.println("厨师做了一碗面条");//4.把食物放在桌子上(修改食物状态为有食物)Table.flag = 1;//5.叫醒等待的消费者开吃Table.lock.notifyAll();}}}}}
}
消费者类
public class Foodie extends Thread {@Overridepublic void run() {//循环while (true) {//同步代码块synchronized (Table.lock) {//判断是否执行到了末尾if (Table.count == 0) {return;} else {//1.判断桌子上是否有食物if (Table.flag == 0) {//2.没有:等待try {Table.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}} else {//3.有:开吃Table.count--;System.out.println("吃货吃了一碗面条,还能吃" + Table.count + "碗");//4.吃食物(修改食物状态为没有食物)Table.flag = 0;//5.叫醒等待的生产者开吃继续做Table.lock.notifyAll();}}}}}
}
实现类
public class Test {public static void main(String[] args) {//生产者线程Cook c = new Cook();//消费者线程Foodie f = new Foodie();//开启线程c.start();f.start();}
}
结果展示

阻塞队列
说明
阻塞队列常用于生产者和消费者的场景,生产者往队列里放元素,消费者从队列里拿元素
当队列为空时,获取元素的消费者线程会等待,直到队列变为非空时,继续获取元素
当队列满时,添加元素的生产者线程会等待,直到队列出现空位,继续添加元素
可以设定阻塞队列的长度,当长度为一时,放一个拿一个,也能实现线程的交替执行
阻塞队列实现类
ArrayBlockingQueue(底层是数组,有界)
LinkedBlockingQueue(底层是链表,无界,但不是真正的无界,最大为int的最大值)
实现类实现了Queue,BlockingQueue,Iterable,Collection接口,表示其可以用迭代器遍历,也是单列集合的一种。
常用方法
方法名 | 说明 |
public void put(E e) | 生产者方法,往阻塞队列里添加元素 |
public E take() | 消费者方法,得到阻塞队列里的元素 |
put方法底层会获取阻塞队列的锁对象,进行上锁,往阻塞队列里添加元素,如果阻塞队列满了就等待,最后进行解锁
take方法底层会获取阻塞队列的锁对象,进行上锁,获取阻塞队列里的元素,如果没有元素就等待,最后进行解锁
因为两个方法底层的锁对象都是和阻塞队列挂钩的,使用同一个阻塞队列,实现锁的唯一性
代码演示
生产者类
import java.util.concurrent.ArrayBlockingQueue;public class Cook extends Thread{private ArrayBlockingQueue<String> queue;//构造方法,得到传递过来的阻塞队列public Cook(ArrayBlockingQueue<String> queue){this.queue = queue;}@Overridepublic void run() {while(true){//不断往阻塞队列里添加元素try {queue.put("食物");System.out.println("厨师做了一碗食物");} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
消费者类
import java.util.concurrent.ArrayBlockingQueue;public class Foodie extends Thread{private ArrayBlockingQueue<String> queue;//构造方法,得到传递过来的阻塞队列public Foodie(ArrayBlockingQueue<String> queue){this.queue = queue;}@Overridepublic void run() {while(true){//不断拿元素try {String take = queue.take();System.out.println(take);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
实现类
import java.util.concurrent.ArrayBlockingQueue;public class Test {public static void main(String[] args) {//创建阻塞队列,设定队列长度为1ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);//创建线程,传递同一个阻塞队列,实现使用同一个阻塞队列//传递运行次数Cook c = new Cook(queue);Foodie f = new Foodie(queue);c.start();f.start();}
}
结果展示
因为put方法,take方法底层有锁,则打印语句写在了锁外面,会出现连续打印的情况,但数据是在锁中的,对实际数据不会有影响,只影响了输出效果
线程的六种状态
新建状态(NEW) 创建线程对象
就绪状态(RUNNABLE) start方法
阻塞状态(BLOCKED) 无法获得锁对象
等待状态(WAITING) wait方法
计时等待(TIMED WAITING) sleep方法
结束状态(TERMINATED ) 全部代码运行完毕
线程池
说明
之前在写多线程的时候,用到多线程就创建,用完就销毁,这样会增加运行时间和浪费资源
所以可以使用线程池,实现线程的复用
主要核心原理
创建一个池子,池子中是空的(可设定池子中最多能有几条线程)
提交任务时,池子会创建线程对象,任务执行完毕,线程归还给池子
下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
但若提交任务时,池中没有空闲线程,也无法创建新的线程(已达上限),任务就会排队等待
线程池代码实现
创建线程池
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
方法名称 | 说明 |
public static ExecutorService newCachedThreadPool() | 创建一个没有上限的线程池 |
public static ExecutorService newFixedThreadPool(int n) | 创建有上限的线程池 |
提交任务
方法名称 | 说明 |
public Future<?> submit | 提交任务 |
所有的任务全部执行完毕,关闭线程池
方法名称 | 说明 |
public void shutdown() | 关闭线程池,一般不需要关闭 |
代码演示
任务类
public class MyRun implements Runnable {@Overridepublic void run() {for (int i = 0; i < 50; i++) {System.out.println(Thread.currentThread().getName() + ":" + i);}}
}
实现类
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Test {public static void main(String[] args) {//创建线程池,设定上限为3ExecutorService e = Executors.newFixedThreadPool(3);//创建任务对象MyRun mr = new MyRun();//提交任务e.submit(mr);e.submit(mr);e.submit(mr);e.submit(mr);e.submit(mr);//关闭任务e.shutdown();}
}
结果部分展示
因为线程池设定上限为3,所以即使提交了5个任务,也只创建了3个线程,并复用这3个线程
自定义线程池
创建自定义线程池:
ThreadPoolExecutor pool = new ThreadPoolExecutor(......);
传递六个参数:
核心线程数量(不能小于0)
线程池中最大线程的数量(最大数量>= 核心线程数量)
空闲时间(值)(不能小于0)
空闲时间(单位)(用TimeUnit指定)
阻塞队列(不能为null)
创建线程的方式(不能为null)
要执行的任务过多时的任务拒绝策略(不能为null)
TimeUnit时间单位 | 说明 |
TimeUnit.SECONDS | 单位秒 |
TimeUnit.MINUTES | 单位分钟 |
阻塞队列 | 说明 |
new ArrayBlockingQueue<>(...) | 创建数组线程池,传递线程池长度 |
new LinkedBlockingQueue<>() | 创建链表线程池 |
创建线程的方式 | 说明 |
Executors.defaultThreadFactory() | 利用线程池工具类,底层创建线程对象,并对线程对象进行一系列的设置 |
任务拒绝策略 | 说明 |
ThreadPoolExecutor.AbortPolicy | 最常用也是默认策略,丢弃任务并抛出 RejectedExecutionException异常 AbortPolicy是一个静态内部类 用外部类.内部类创建该对象 |
不断的提交任务时,核心线程,阻塞队列,临时线程的使用时机:
当核心线程满时,再提交任务就会排队
当核心线程满,队列满时,会创建临时线程
当核心线程满,队列满,临时线程满时,会触发任务拒绝策略
代码演示
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class Test {public static void main(String[] args) {//创建自定义线程池,传递六个参数ThreadPoolExecutor pool = new ThreadPoolExecutor(3,6,60,TimeUnit.SECONDS,new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());//提交任务pool.submit(new MyRun());pool.submit(new MyRun());pool.submit(new MyRun());}
}
线程池多大合适
CPU密集型运算:最大并行数+1
I/O密集型运算:
最大并行数 x 期望CPU利用率 x [(CPU计算时间 + 等待时间) / CPU 计算时间]