一、线程池
1.1 什么是线程池
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
总的来说 线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。
1.2 为什么使用线程池?
一个线程大约占用的内存1M.
解决频繁创建线程和销毁线程消耗的性能。
解决大量创建线程而导致的内存泄露问题。
1.3 如何创建线程池
java中其实提供了两种方式:
第一种: 通过工具类完成线程池的创建.[Executors]. 语法简单。但是阿里巴巴不建议使用
第二种: 通过线程池类: ThreadPoolExecutor类. 语法复杂,但是阿里巴巴建议使用。灵活
线程的根接口: Executor. 里面只有一个方法: execute子接口: ExecutorService。
第一种方案Executors:
- 固定大小的线程池对象newFixedThreadPool
- 单一线程池: newSingleThreadExecutor
- 可变线程池: newCachedThreadPool
- 延迟线程池: newScheduledThreadPool
public static void main(String[] args) {
//创建一个固定大小的线程池。返回类型为ExecutorService.
// ExecutorService executorService = Executors.newFixedThreadPool(5);
//创建单一线程池【池子里面只有一个线程的对象】适合任务顺序执行的。
// ExecutorService executorService = Executors.newSingleThreadExecutor();
//创建可变线程池。【池子中线程的数量会随着任务的大小而变化】
// ExecutorService executorService = Executors.newCachedThreadPool();
//延迟线程池:[指定时间后执行]
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
//执行线程任务。Runnable Callable. Integer.MAX()整数的最大值。
// for(int i=0;i<10;i++) {
//1.必须传递Runnable对象。[1]自己创建一个类实现Runnable接口 [2]匿名内部类对象 [3]lambda表达式: 前提接口必须为函数式接口。
// executorService.execute(new My());
// executorService.execute(new Runnable() {
// @Override
// public void run() {
// System.out.println(Thread.currentThread().getName()+"*************");
// }
// });
//表示10秒后执行任务代码块。
// executorService.schedule(()->{
// System.out.println(Thread.currentThread().getName()+"~~~~~~~~~~~~~~~");
// },10, TimeUnit.SECONDS);
executorService.scheduleAtFixedRate(()->{
System.out.println(Thread.currentThread().getName());
},5,2, TimeUnit.SECONDS);
// }
//关闭线程池
}
execute和submit方法区别?
1. 这两个方法都是用来执行线程任务,但是execute属于Executor类中的方法,而submit属于ExecutorService接口中的方法。 而且submit可以执行runnable和callable类型的任务,而 execute只能执行Runnable类型的任务。 submit执行完任务后有返回结果。
第二种方式ThreadPoolExecutor
package com.zql.test05;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Test02 {
public static void main(String[] args) {
//核心参数的意思
/**
* int corePoolSize,核心线程数的个数 2
* int maximumPoolSize,最大线程数的个数 5
* long keepAliveTime,非核心线程的存活时间
* TimeUnit unit,存活时间的单位
* BlockingQueue<Runnable> workQueue,堵塞队列 3
*/
// 创建一个固定大小的阻塞队列,用于存储待执行的任务
BlockingQueue<Runnable> workQueue=new ArrayBlockingQueue(5);
// 初始化一个线程池执行器,设定核心线程数为2,最大线程数为6,线程闲置时间为10秒,队列大小为5
ThreadPoolExecutor poolExecutor=new ThreadPoolExecutor(2,6,10, TimeUnit.SECONDS,workQueue);
// 提交10个任务给线程池执行
for (int i = 0; i < 10; i++) {
poolExecutor.submit(()->{
// 打印当前执行任务的线程名称
System.out.println(Thread.currentThread().getName()+"执行了");
});
}
}
}
corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
我们可以通过下面的场景理解ThreadPoolExecutor中的各个参数;
a客户(任务)去银行(线程池)办理业务,但银行刚开始营业,窗口服务员还未就位(相当于线程池中初始线程数量为0),
于是经理(线程池管理者)就安排1号工作人员(创建1号线程执行任务)接待a客户(创建线程);
在a客户业务还没办完时,b客户(任务)又来了,于是经理(线程池管理者)就安排2号工作人员(创建2号线程执行任务)接待b客户(又创建了一个新的线程);假设该银行总共就2个窗口(核心线程数量是2);
紧接着在a,b客户都没有结束的情况下c客户来了,于是经理(线程池管理者)就安排c客户先坐到银行大厅的座位上(空位相当于是任务队列)等候,
并告知他: 如果1、2号工作人员空出,c就可以前去办理业务;
此时d客户又到了银行,(工作人员都在忙,大厅座位也满了)于是经理赶紧安排临时工(新创建的线程)在大堂站着,手持pad设备给d客户办理业务;
假如前面的业务都没有结束的时候e客户又来了,此时正式工作人员都上了,临时工也上了,座位也满了(临时工加正式员工的总数量就是最大线程数),
于是经理只能按《超出银行最大接待能力处理办法》(饱和处理机制)拒接接待e客户;
最后,进来办业务的人少了,大厅的临时工空闲时间也超过了1个小时(最大空闲时间),经理就会让这部分空闲的员工人下班.(销毁线程)
但是为了保证银行银行正常工作(有一个allowCoreThreadTimeout变量控制是否允许销毁核心线程,默认false),即使正式工闲着,也不得提前下班,所以1、2号工作人员继续待着(池内保持核心线程数量);
案例
综合案例-秒杀商品
案例介绍:
假如某网上商城推出活动,新上架10部新手机免费送客户体验,要求所有参与活动的人员在规定的时间同时参与秒杀挣抢,假如有20人同时参与了该活动,请使用线程池模拟这个场景,保证前10人秒杀成功,后10人秒杀失败;
要求:
1:使用线程池创建线程
2:解决线程安全问题
思路提示:
1:既然商品总数量是10个,那么我们可以在创建线程池的时候初始化线程数是10个及以下,设计线程池最大数量为10个;
2:当某个线程执行完任务之后,可以让其他秒杀的人继续使用该线程参与秒杀;
3:使用synchronized控制线程安全,防止出现错误数据;
代码步骤:
1:编写任务类,主要是送出手机给秒杀成功的客户;
2:编写主程序类,创建20个任务(模拟20个客户);
3:创建线程池对象并接收20个任务,开始执行任务;
主程序类,测试任务类
package com.zql.test06;
/**
* MyTask 类实现了 Runnable 接口,用于表示一个可运行的任务。
* 该任务模拟了一个简单的秒杀系统,每个任务代表一个用户尝试秒杀手机。
*/
public class MyTask implements Runnable{
/**
* 秒杀手机的数量,静态变量,被所有任务共享。
* 初始数量为10,表示有10部手机可供秒杀。
*/
private static int count = 10;
/**
* 用户的名字,用于标识哪个用户在执行任务。
*/
private String name;
/**
* 构造函数,初始化用户名字。
*
* @param name 用户的名字。
*/
public MyTask(String name)
{
this.name = name;
}
/**
* 当线程执行时,该方法被调用。
* 主要逻辑是尝试秒杀手机,通过同步代码块保证线程安全。
*/
@Override
public void run() {
System.out.println(name+"进入秒杀系统----");
synchronized (MyTask.class){
// 检查手机库存是否还有剩余
if (count > 0){
System.out.println(name+"抢到了"+count+"号手机,秒杀成功。");
count--; // 成功秒杀后,减少手机库存数量
}else {
System.out.println(name+"秒杀失败。");
}
}
}
}
package com.zql.test06;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 测试类,用于演示固定大小线程池的使用。
* @author
* @date 2024-07-09
*/
public class Test01 {
/**
* 程序入口。
* 创建一个固定大小的线程池,并提交20个任务给线程池执行。
* @param args 命令行参数
*/
public static void main(String[] args) {
// 创建一个固定大小为10的线程池
ExecutorService executorService= Executors.newFixedThreadPool(10);
// 循环提交20个任务给线程池
for (int i = 0; i <=20; i++) {
// 创建任务,并传递任务参数
MyTask myTask=new MyTask("客户"+i);
// 执行任务
executorService.execute(myTask);
}
}
}