一、线程池
1. 什么是线程池?
从JDK 5开始出现,线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。(这里的任务就是实现了Runnable或Callable接口的实例对象)
2.线程池有什么优势?
① 降低资源消耗(重复利用线程池中的线程)
② 提高响应速度(减少了创建新线程的时间)
③ 便于线程管理(线程池统一分配,调度线程)
二、ExecutorService接口
//Executor是线程的顶级接口,提供了包括线程使用,调度等功能。
//Executor接口只定义了一个抽象方法void execute(Runnable command):在将来的某个时间执行给定的命令。
public interface ExecutorService extends Executor {...}
1.获取ExecutorService
可以利用Executors工具类中的静态方法获取ExecutorService,常见的如下:
(1)定长线程池(newFixedThreadPool)
特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
应用场景:控制线程最大并发数。
//newFixedThreadPool举例说明
@Test
public void test01(){
//1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
//2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task = new Runnable() {//此时线程池中只有一个线程
public void run() {
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"正在执行任务...");
}
}
};
//3. 向线程池提交任务
fixedThreadPool.execute(task);
//4.关闭线程池
fixedThreadPool.shutdown();
}
(2)定时线程池(ScheduledThreadPool )
特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。
应用场景:执行定时或周期性的任务。
//1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
//2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task = new Runnable() {
public void run() {
System.out.println("正在执行任务...");
}
};
//3. 向线程池提交任务
scheduledThreadPool.schedule(task, 5, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task, 10, 1000, TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
(3)可缓存线程池(CachedThreadPool)
特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
应用场景:执行大量、耗时少的任务。
// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("正在执行任务...");
}
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);
(4)单线程化线程池(SingleThreadExecutor)
特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。
// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);
三、ThreadPoolExecutor
ExecutorService 是接口,无法接创建对象,因此线程池的真正实现类是 ThreadPoolExecutor,其构造方法有如下4种:
//1.创建一个新的 ThreadPoolExecutor与给定的初始参数和默认线程工厂和拒绝执行处理程序。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
//2.创建一个新的 ThreadPoolExecutor与给定的初始参数和默认线程工厂。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
//3.创建一个新的 ThreadPoolExecutor与给定的初始参数和默认拒绝执行处理程序。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
//4.创建一个新 ThreadPoolExecutor给定的初始参数。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
当构造函数出现以下条件,会抛出如下异常:
从以上可以看出,ThreadPoolExecutor类构造方法都具有如下的参数:
2.线程池工作流程
2.1 处理任务的流程和优先级
核心线程(corePoolSize)—>任务队列(workQueue)—>最大线程(maximumPoolSize)
当线程池中的线程数量 > 核心线程(corePoolSize)时,若某线程(非核心线程)的空闲时间 > 闲置超时时长(keepAliveTime) ,线程将被终止;(通过这样的策略,线程池可动态调整池中的线程数)
① 是否达到核心线程数量?没达到,创建一个工作线程来执行任务。
② 工作队列是否已满?没满,则将新提交的任务存储在工作队列里。
③ 是否达到线程池最大数量?没达到,则创建一个新的工作线程来执行任务。最后,执行拒绝策略来处理这个任务。
// 1. 创建线程池
// 创建时,通过配置线程池的参数,从而实现自己所需的线程池
Executor threadPool = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE,
TimeUnit.SECONDS,
sPoolWorkQueue,
sThreadFactory
);
// 注:在Java中,已内置4种常见线程池,下面会详细说明
// 2. 向线程池提交任务:execute()
// 说明:传入 Runnable对象
threadPool.execute(new Runnable() {
@Override
public void run() {
... // 线程执行任务
}
});
// 3. 关闭线程池shutdown()
threadPool.shutdown();
// 关闭线程的原理
// a. 遍历线程池中的所有工作线程
// b. 逐个调用线程的interrupt()中断线程(注:无法响应中断的任务可能永远无法终止)
// 也可调用shutdownNow()关闭线程:threadPool.shutdownNow()
// 二者区别:
// shutdown:设置线程池的状态为 SHUTDOWN,然后中断所有没有正在执行任务的线程
// shutdownNow:设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
// 使用建议:一般调用shutdown()关闭线程池;若任务不一定要执行完,则调用shutdownNow()
线程池的状态:Running、ShutDown、Stop、Tidying、Terminated。
四、线程池使用案例
假如某网上商城推出活动,新上架5部新手机免费送客户体验,要求所有参与活动的人员在规定的时间同时参与秒杀挣抢,假设有10人同时参与了该活动,请使用线程池模拟这个场景,保证前5人秒杀成功,后5人秒杀失败;
要求:
① 使用线程池创建线程
② 解决线程安全问题
思路提示:
① 既然商品总数量是5个,那么我们可以在创建线程池的时候初始化线程数是5个及以下,设计线程池最大数量为5个;
② 当某个线程执行完任务之后,可以让其他参与秒杀的人继续使用该线程参与秒杀;
③ 使用synchronized控制线程安全
代码步骤:
① 编写任务类,主要是送出手机给秒杀成功的客户;
② 编写主程序类,创建10个任务(模拟10个客户);
③ 创建线程池对象并接收10个任务,开始执行任务。
/**
* ThreadPoolExecutor线程池模拟秒杀系统,10个客户对5个商品进行秒杀
* @author wds
* @date 2021-12-02-18:40
*/
public class ThreadPoolTest02 {
public static void main(String[] args) {
//1.创建线程池(corePoolSize:3,maximumPoolSize:5,keepAliveTime:1,TimeUnit.MILLISECONDS,new LinkedBlockingDeque<>(15))
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 5, 1,
TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(15));
//2.创建任务对象(实现类对象)
for (int i=1;i<=10;i++){
MyTask myTask = new MyTask("客户" + i);
// Future<?> submit = threadPoolExecutor.submit(myTask);
// System.out.println(submit);
//问题一:execute(Runnable command)和submit(Runnable task)有什么区别?
threadPoolExecutor.execute(myTask);
}
//3.关闭线程池
// threadPoolExecutor.shutdownNow();
//问题二:shutdown()和shutdownNow()有什么区别?
threadPoolExecutor.shutdown();
}
}
//任务对象(实现Runnable接口,重写run()方法)
class MyTask implements Runnable{
private static int num = 5;//商品数量
private String username;//客户姓名
public MyTask(String username) {
this.username = username;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(username+"正在通过"+name+"参与秒杀");
try {
Thread.sleep(100);//模拟秒杀需要的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (MyTask.class){
if(num>0){
System.out.println(username+"使用"+name+"秒杀:"+num--+"号商品成功了!");//秒杀成功,商品数减1
}else{
System.out.println(username+"使用"+name+"秒杀商品失败了...");
}
}
}
}