1. 什么是线程池
线程池是一种并发编程的技术,用于管理和复用线程。在程序运行过程中,创建和销毁线程是一项昂贵的操作,而线程池的主要目的是通过预先创建一组线程,并在需要时重复利用它们,来减少线程创建和销毁的开销,从而提高程序的性能和响应速度。
2. 为什么使用线程池
一个线程大约占用的内存1M.
- 解决频繁创建线程和销毁线程消耗的性能。
- 解决大量创建线程而导致的内存泄露问题。
3. 如何创建线程池
java中提供了两种方式来创建线程池:
第一种: 通过工具类完成线程池的创建.[Executors]. 语法简单。但是阿里巴巴不建议使用。
第二种: 通过线程池类: ThreadPoolExecutor类. 语法复杂,但是阿里巴巴建议使用。因为它比较灵活。
线程的根接口: Executor. 里面只有一个方法: execute
子接口: ExecutorService。
3.1 使用工具类创建线程池
使用工具类创建线程池有四种方式,分别是:
- 固定大小的线程池对象newFixedThreadPool
- 单一线程池: newSingleThreadExecutor
- 可变线程池: newCachedThreadPool
- 延迟线程池: newScheduledThreadPool
3.1.1 固定大小的线程池
固定大小的线程池是一种特定类型的线程池,其特点是在初始化时就确定了固定数量的线程,并且一直保持这个数量不变。
public class Thread01 {
public static void main(String[] args) {
// 固定大小的线程池对象
// 里面的5为线程数量
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executorService.execute(
()-> System.out.println(Thread.currentThread().getName()+"~~~~~~~~"));
}
// 关闭线程池
executorService.shutdown();
}
}
运行结果:
通过运行结果可以看出,线程只有1~5,5个线程,不会超出给定的线程数量。
3.1.2 单一线程池
单一线程池是一种特殊的线程池,它只包含一个单独的工作线程。这意味着所有提交给单一线程池的任务都会由同一个线程顺序执行,这个线程会按照任务提交的顺序依次执行它们。
public static void main(String[] args) {
// 单一线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
executorService.execute(()-> System.out.println(Thread.currentThread().getName()+"~~~~~~~~"));
}
// 关闭线程池
executorService.shutdown();
}
运行结果:
通过运行结果可以看出,从始至终都只有一个线程执行程序,因为单一线程池中只有一个线程。
3.1.3 可变线程池
可变线程池是一种具有动态调整线程数量的线程池,其大小可以根据需要自动调整。
public static void main(String[] args) {
// 可变线程池
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 11; i++) {
executorService.execute(()-> System.out.println(Thread.currentThread().getName()+"~~~~~~~~"));
}
// 关闭线程池
executorService.shutdown();
}
运行结果:
根据运行结果可以看出,打印11次,达到了十个线程,线程3打印了两次,这是因为打印第11次的时候,线程3执行完毕进入空闲状态,并且抢到了第十一次打印的线程。
如果我们把打印的次数换成50次
for (int i = 0; i < 50; i++) {
executorService.execute(()-> System.out.println(Thread.currentThread().getName()+"~~~~~~~~"));
}
运行结果:
根据运行结果可以看出,此时达到了16个线程,根据两次对比可以看出,可变线程池的线程数量是根据需要自行调整的。
3.1.4 延迟线程池
延迟线程池是一种专门用于处理需要延迟执行的任务的线程池。在这种线程池中,任务可以设定一个延迟时间,在指定的延迟时间之后才会被执行。延迟线程池通常用于处理需要在未来某个时间点执行的任务,比如定时任务或者某些需要等待特定条件满足后再执行的任务。
public static void main(String[] args) {
// 延迟线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);// 线程数
for (int i = 0; i < 10; i++) {
scheduledExecutorService.schedule(()-> System.out.println(Thread.currentThread().getName()+"~~~"),1, TimeUnit.SECONDS);
}
for (int i = 0; i < 10; i++) {
scheduledExecutorService.scheduleAtFixedRate(()-> System.out.println(Thread.currentThread().getName()+"~~~"),1,2, TimeUnit.SECONDS);
}
// 关闭线程池
scheduledExecutorService.shutdown();
}
在这个代码中,第一个for循环中的schedule方法中有三个参数,第二个for循环中的scheduleAtFixedRate方法中有四个参数。
schedule方法中,第一个参数为要执行的任务,第二个参数为初始延迟时间,第三个参数为时间单位。即这个方法在1秒之后执行,会一次性把十次打印都打印出来,只是总体任务时间延迟。
scheduleAtFixedRate方法中,第一个参数为要执行的任务,第二个参数为初始延迟时间,第三个参数为延迟周期,第四个参数为时间单位。即这个方法在1秒之后执行,每过2秒打印一次,打印十次后结束。
3.2 通过线程池类创建线程池
在Java中,可以通过
ExecutorService
接口的实现类来创建线程池,这些实现类通常是ThreadPoolExecutor
或者ScheduledThreadPoolExecutor
的实例化对象。这两个类分别实现了基本的线程池和定时任务线程池。
下面通过ThreadPoolExecutor实现类来创建基本的线程池:
public static void main(String[] args) {
// 创建堵塞队列,最大为5个
BlockingQueue<Runnable> workQueue=new ArrayBlockingQueue(5);
ThreadPoolExecutor poolExecutor=new ThreadPoolExecutor(2,6,10, TimeUnit.SECONDS,workQueue);
for (int i = 0; i <11; i++) {
poolExecutor.submit(()->{
System.out.println(Thread.currentThread().getName()+"~~~~~~~~~~~~~~~~");
});
}
// 关闭线程池
poolExecutor.shutdown();
}
在这个代码中,new ThreadPoolExecutor里参数的含义为:
- int corePoolSize,核心线程数的个数 2
- int maximumPoolSize,最大线程数量 5
- long keepAliveTime, 非核心线程允许空闲的时间 10
- TimeUnit unit, 时间的单位 秒,TimeUnit.SECONDS,workQueue
- BlockingQueue workQueue 堵塞队列 5
运行结果:
根据运行结果可以看出:最大线程数为6。
我们可以通过下面的场景理解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个任务,开始执行任务;
主程序类,测试任务类
实体类:
public class Phone implements Runnable{
private static int number = 10;
private String name;
public Phone() {
}
public Phone(String name) {
this.name = name;
}
@Override
public void run() {
// 双重锁
if (number > 0){
synchronized (Phone.class){
if (number > 0){
System.out.println(name+"秒杀成功,抢到了"+(11-number)+"号手机");
number--;
return;
}
}
}
System.out.println(name+"秒杀失败");
}
}
测试类:
public class Test {
public static void main(String[] args){
// 创建堵塞队列,最大为5个
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, workQueue);
for (int i = 0; i < 20; i++) {
Phone phone = new Phone("用户"+(i+1));
threadPoolExecutor.execute(phone);
}
threadPoolExecutor.shutdown();
}
}
运行结果: