一、线程通信
线程是并发并行执行,表现是线程间随机执行
线程是共享Java进程的内存,可以使用共享变量
为什么还需要线程通信?
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序
假如有这样一个场景:
Java中,设置进程的优先级(setPriority)不行!因为判断优先级中不仅仅依据这一个指标,一般开发都不会依赖优先级来表示线程的执行顺序
Object中,有三个方法:
- wait():让当前线程持有的对象锁释放,并等待
- notify():唤醒使用同一个对象,调用wait进入等待的线程,重新竞争对象锁(如果有多个线程调用wait处于等待,notify是随机唤醒一个,notifyAll是全部唤醒)
- notifyAll()
必须使用在synchronized同步代码块/同步方法中:哪个对象加锁,就使用哪个对象wait,notify
//这个写法:不满足,就一直等
synchronized(对象){
while(某个不满足执行条件){
//线程就需要等待
加锁的对象.wait();//当前线程释放持有的对象锁,且当前线程就由运行态转变为等待状态
}
//满足条件,可以做事情
doSomething()
//通知调用了wait等待的线程,可以唤醒再次竞争锁
}
通知的方式就是nottify()或者notifyAll(),不是调用notify,就马上唤醒,而是synchronized结束(当前线程释放锁)以后,唤醒线程,来再次竞争锁
通知(notify,notifyAll),等待(wait)结合起来,就可以表现多个线程之间,满足一定条件做事情(做了事情,条件就可能改变了,需要发起通知),不满足则等待
二、实现面包房案例
public class 面包房 {
//面包房的库存:0-100
private static int STORE;
public static void main(String[] args) {
//同时有两个生产者生产面包
面包师傅 producer = new 面包师傅();
for(int i=0;i<2;i++){
new Thread(producer,"面包师傅-"+(i+1)).start();
}
消费者 consumer = new 消费者();
for(int i=0;i<2;i++){
new Thread(consumer,"消费者-"+(i+1)).start();
}
}
//生产者/面包师傅:每次生产3个面包
private static class 面包师傅 implements Runnable{
private int num=3;
@Override
public void run() {
try {
while (true) {//不停的生产
synchronized (面包房.class) {
while (num + STORE > 100) {//不满足生产条件:等待
面包房.class.wait();
}
//满足条件:生产
STORE += num;
//打印看看,当他等待一小段时间(涉及synchronized优化的原理)
System.out.println(Thread.currentThread().getName() + "生产了面包,库存:" + STORE);
Thread.sleep(500);
//通知wait等待的线程
面包房.class.notifyAll();//建议使用全部通知(极端情况可能出现所有线程都等待的情况)
}
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费者:每次消费1个面包
private static class 消费者 implements Runnable{
private int num=1;
@Override
public void run() {
try {
while(true) {//不停的消费
synchronized (面包房.class) {
while (STORE - num < 0) {//不满足消费条件:等待
面包房.class.wait();
}
//满足条件:消费
STORE -= num;
//打印看看,当他等待一小段时间(涉及synchronized优化的原理)
System.out.println(Thread.currentThread().getName() + "消费了面包,库存:" + STORE);
Thread.sleep(500);
//通知wait等待的线程
面包房.class.notifyAll();//建议使用全部通知(极端情况可能出现所有线程都等待的情况)
}
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
三、阻塞式队列
1.阻塞队列是什么
阻塞队列是一种特殊的队列,也遵守”先进先出“的原则
阻塞队列是一种线程安全的数据结构,并且具有以下特性:
- 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素
- 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素
阻塞队列的一个典型应用场景就是==”生产者消费者模型“,这是一种非常典型的开发模型
阻塞队列一般用于做任务的解耦和削峰==
解耦: 生产和消费,不需要一处代码完成,生产和消费的代码完全解耦
削峰: 生产速度快于消费速度,把任务对象放在队列,等消费慢慢执行;
消费速度快于生产速度,就是消费阻塞等待有新的任务
JDK也提供了一个阻塞队列的接口(BlockingQueue< E >),有多个实现类
2.生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等代消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取
- 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
比如在”秒杀“场景下,服务器同一时刻可能会收到大量的支付请求,如果直接处理这些支付请求,服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程),这个时候就可以把这些请求都放到一个阻塞队列中,然后再由消费者线程慢慢的来处理每个支付请求
这样做可以有效进行”削峰“,防止服务器被突然到来的一波请求直接冲垮
- 阻塞队列也能使生产者和消费者之间解耦
比如过年一家人一起包饺子,一般都是有明确分工,比如一个人负责擀饺子皮,其他人负责包,擀饺子皮的人就是”生产者“,包饺子的人就是”消费者“
擀饺子皮的人不关心包饺子的人是谁(能包就行,无论是手工包,借助工具,还是机器包),包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行,无论是擀面杖擀得,还是直接去超市买的)
3.阻塞队列实现
public class 阻塞队列 {
//使用数组实现一个循环队列
private Runnable[] tasks;
//容量:存放的元素数量
//多线程存取元素时
private int count;
//存放元素的位置
private int putIndex;
//取元素的位置
private int takeIndex;
public 阻塞队列(int size) {
tasks=new Runnable[size];
}
//存放元素:线程安全的
public void put(Runnable task){
try {
synchronized (阻塞队列.class){
while(count==tasks.length){
阻塞队列.class.wait();
}
//放到数组中
tasks[putIndex]=task;
//更新索引
putIndex = (putIndex+1)%tasks.length;
//更新存放数量
count++;
//通知
阻塞队列.class.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public Runnable take(){
try {
synchronized (阻塞队列.class){
while(count==0){
阻塞队列.class.wait();
}
//取元素
Runnable task = tasks[takeIndex];
//更新索引
takeIndex = (takeIndex+1)% tasks.length;
//更新存放数量
count--;
//通知
阻塞队列.class.notifyAll();
return task;
}
} catch (InterruptedException e) {
throw new RuntimeException("存放元素出错",e);
}
}
//测试一下
public static void main(String[] args) {
Runnable consumer = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"消费");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
阻塞队列 blockingQueue = new 阻塞队列(20);
for(int i=0;i<5;i++){
//模拟生产者:生产了五个任务
blockingQueue.put(consumer);//main线程生产
}
//模拟消费者:不停的消费任务
new Thread(new Runnable() {
@Override
public void run() {
while(true){
Runnable task = blockingQueue.take();
task.run();
}
}
},"消费者").start();
}
}
四、线程池
我们之前学的常量池、数据库连接池,包括这里的线程池,都是属于共享/缓存资源,使用的时候直接从池里边拿
常量池:类似缓存,先从池中取,取不到创建并存放在池里边
数据库连接池和线程池:初始化,就创建一定数量的元素
线程池的作用:减少线程创建和销毁的开销
1.相关api
便捷的api:工作不要使用!
(1)可能使用无边界的阻塞队列,如果生产速度过快,可能会在某个时间,导致内存不够,出现OOM(内存溢出)
(2)没有设置拒绝策略,或使用JDK提供的这4种(最多可以考虑使用CallerRunsPolicy),都不合适:可能需要把队列满了的时候,提交的任务保存在日志或数据库
阻塞队列一定要设置大小,拒绝策略需要自行扩展(比如把任务记录在日志或数据库)
使用线程池:就是提交任务到线程池的阻塞队列中
- execute(Runnable task)
- submit(Runnable task)或者submit(Callable task)
线程池的工作流程
五、定时器
在等待一定时间之后,再执行任务
应用:tomcat对超时的session进行销毁,redis(nosql,内存数据库,缓存)中提供超时的处理
Java中的定时器
- Timer:不推荐
- 定时任务的线程池:推荐