1. 线程池概述
什么是线程池
线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;
为什么使用线程池
使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行运行压力;当然了,使用线程池的原因不仅仅只有这些,我们可以从线程池自身的优点上来进一步了解线程池的好处;
使用线程池有哪些优势
1: 线程和任务分离,提升线程重用性;
2: 控制线程并发数量,降低服务器压力,统一管理所有线程;
3: 提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;
应用场景介绍
1:网购商品秒杀
2:云盘文件上传和下载
3:12306网上购票系统等
只要有并发的地方、任务数量大或小、每个任务执行时间长或短的都可以使用线程池;
只不过在使用线程池的时候,注意一下设置合理的线程池大小即可;(关于如何合理设置线程池大小在后面的章节中讲解)
2. 线程池原理
2.1 Java内置线程池原理剖析
我们要想自定义线程池,必须先了解线程池的工作原理,才能自己定义线程池;
这里我们通过观察java中ThreadPoolExecutor的源码来学习线程池的原理;
我们可以通过下面的场景理解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号工作人员继续待着(池内保持核心线程数量);
2.2 参数设计分析
通过观察Java中的内置线程池参数讲解和线程池工作流程总结,我们不难发现,要设计一个好的线程池,就必须合理的设置线程池的4个参数;那到底该如何合理的设计4个参数的值呢?我们一起往下看.
1:核心线程数(corePoolSize)
核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定,例如:执行一个任务需要0.1秒,系统百分之80的时间每秒都会产生100个任务,那么要想在1秒内处理完这100个任务,就需要10个线程,此时我们就可以设计核心线程数为10;当然实际情况不可能这么平均,所以我们一般按照8020原则设计即可,既按照百分之80的情况设计核心线程数,剩下的百分之20可以利用最大线程数处理;
2:任务队列长度(workQueue)
任务队列长度一般设计为:核心线程数/单个任务执行时间*2即可;例如上面的场景中,核心线程数设计为10,单个任务执行时间为0.1秒,则队列长度可以设计为200;
3:最大线程数(maximumPoolSize)
最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定:例如:上述环境中,如果系统每秒最大产生的任务是1000个,那么,最大线程数=(最大任务数-任务队列长度)*单个任务执行时间;既: 最大线程数=(1000-200)*0.1=80个;
4:最大空闲时间(keepAliveTime)
这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可;
2.3 自定义线程池
1:编写任务类(MyTask),实现Runnable接口;
2:编写线程类(MyWorker),用于执行任务,需要持有所有任务;
3:编写线程池类(MyThreadPool),包含提交任务,执行任务的能力;
4:编写测试类(MyTest),创建线程池对象,提交多个任务测试;
package com.hjy.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MyTask implements Runnable {
private int id;
//由于run方法是重写接口中的方法,因此id这个属性初始化可以利用构造方法完成
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println("线程: " + name + " 即将执行任务: " + id);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程: " + name + " 完成了任务: " + id);
}
}
package com.hjy.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
public class MyWorker extends Thread {
private String name;//保存线程的名字
private List<Runnable> tasks;
@Override
public void run() {
//判断集合中是否有任务,只要有,就一直执行任务
while (tasks.size() > 0) {
Runnable r = tasks.remove(0);
r.run();
}
}
}
package com.hjy.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MyThreadPool {
// 1:任务队列 集合 需要控制线程安全问题
private List<Runnable> tasks = Collections.synchronizedList(new LinkedList<>());
//2: 当前线程数量
private int num;
//3: 核心线程数量
private int corePoolSize;
//4: 最大线程数量
private int maxSize;
//5: 任务队列的长度
private int workSize;
// 1. 提交任务
public void submit(Runnable r) {
if (tasks.size() > workSize) {
System.out.println("任务:"+r+"被丢弃了...");
}else {
tasks.add(r);
// 执行任务
execTask(r);
}
}
private void execTask(Runnable r) {
if (num < corePoolSize) {
new MyWorker("核心线程:"+num, tasks).run();
num ++;
}else if (num <=maxSize) {
new MyWorker("非核心线程:"+num,tasks).start();
num++;
}else {
System.out.println("任务:"+r+" 被缓存了...");
}
}
}
package com.hjy.pojo;
public class Main {
public static void main(String[] args) {
MyThreadPool myThreadPool = new MyThreadPool();
myThreadPool.setCorePoolSize(2);
myThreadPool.setMaxSize(4);
myThreadPool.setWorkSize(20);
for (int i = 0; i < 10; i++) {
MyTask myTask = new MyTask(i);
myThreadPool.submit(myTask);
}
}
}
3. Java内置线程池
3.1 ExecutorService 介绍与基本方法
ExecutorService接口是java内置的线程池接口,通过学习接口中的方法,可以快速的掌握java内置线程池的基本使用
void shutdown()
// 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
List<Runnable> shutdownNow()
// 停止所有正在执行的任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
<T> Future<T> submit(Callable<T> task)
// 执行带返回值的任务,返回一个Future对象。
Future<?> submit(Runnable task)
// 执行 Runnable 任务,并返回一个表示该任务的 Future。
<T> Future<T> submit(Runnable task, T result)
// 执行 Runnable 任务,并返回一个表示该任务的 Future。
3.2 ExecutorService 实现类获取与使用
获取ExecutorService可以利用JDK中的Executors 类中的静态方法,常用获取方式如下:
static ExecutorService newCachedThreadPool()
// 创建一个默认的线程池对象,里面的线程可重用,且在第一次使用时才创建
static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
// 线程池中的所有线程都使用ThreadFactory来创建,这样的线程无需手动启动,自动执行;
static ExecutorService newFixedThreadPool(int nThreads)
// 创建一个可重用固定线程数的线程池
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
// 创建一个可重用固定线程数的线程池且线程池中的所有线程都使用ThreadFactory来创建。
static ExecutorService newSingleThreadExecutor()
// 创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
// 创建一个使用单个 worker 线程的 Executor,且线程池中的所有线程都使用ThreadFactory来创建。
接下来我们以 newCachedThreadPool 方法为例, 介绍使用
public static void test1() {
// 获取线程池对象
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i <10; i++) {
int finalI = i;
// 提交任务
executorService.submit(() -> {
System.out.println("任务"+finalI+"被提交,线程"+Thread.currentThread().getName()+"正在执行");
});
}
}
public static void test2() {
// 获取线程池对象, 这次是自定义的 Thread 工厂
ExecutorService executorService = Executors.newCachedThreadPool(target -> new Thread(target));
for (int i = 0; i <10; i++) {
int finalI = i;
// 提交任务
executorService.submit(() -> {
System.out.println("任务"+finalI+"被提交,线程"+Thread.currentThread().getName()+"正在执行");
});
}
}
3.3 ScheduledExecutorService 的使用
ScheduledExecutorService是ExecutorService的子接口,具备了延迟运行或定期执行任务的能力
常用获取方式如下:
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
// 创建一个可重用固定线程数的线程池且允许延迟运行或定期执行任务;
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
// 创建一个可重用固定线程数的线程池且线程池中的所有线程都使用ThreadFactory来创建,且允许延迟运行或定期执行任务;
static ScheduledExecutorService newSingleThreadScheduledExecutor()
// 创建一个单线程执行程序,它允许在给定延迟后运行命令或者定期地执行。
static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory)
// 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。
常用方法如下:
该方法都用于提交 任务 ,对标 submit 方法
<V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)
// 延迟时间单位是unit,数量是delay的时间后执行callable。
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
// 延迟时间单位是unit,数量是delay的时间后执行command。
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
// 延迟时间单位是unit,经历了 initialDelay 的时间后,每间隔period时间重复执行一次command, 这里任务执行的时间是算在间隔时间中的
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
// 创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。
public static void test1() {
// 获取ScheduledExecutorService 对象
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
for (int i = 0; i <10; i++) {
int finalI = i;
// 提交任务, 一秒钟后执行
executorService.schedule(() -> {
System.out.println("任务"+finalI+"被提交,线程"+Thread.currentThread().getName()+"正在执行");
},1,TimeUnit.SECONDS);
}
}
public static void test3() {
// 获取ScheduledExecutorService 对象
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
for (int i = 0; i <10; i++) {
int finalI = i;
// 提交任务, 一秒钟后执行, 之后每两秒执行一次
executorService.scheduleAtFixedRate(() -> {
System.out.println("任务"+finalI+"被提交,线程"+Thread.currentThread().getName()+"正在执行");
},1,2,TimeUnit.SECONDS);
}
}
3.4 Future 对象
我们刚刚在学习java内置线程池使用时,没有考虑线程计算的结果,但开发中,我们有时需要利用线程进行一些计算,然后获取这些计算的结果,而java中的Future接口就是专门用于描述异步计算结果的,我们可以通过Future 对象获取线程计算的结果;
Future 的常用方法如下:
boolean cancel(boolean mayInterruptIfRunning)
// 试图取消对此任务的执行。
V get()
// 等待计算完成,然后获取其结果。
V get(long timeout, TimeUnit unit)
// 最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。
boolean isCancelled()
// 如果在任务正常完成前将其取消,则返回 true。
boolean isDone()
// 如果任务已完成,则返回 true
public class FutureDemo {
public static void main(String[] args) throws Exception {
//1:获取线程池对象
ExecutorService es = Executors.newCachedThreadPool();
//2:创建Callable类型的任务对象
Future<Integer> f = es.submit(new MyCall(1, 1));
//3:判断任务是否已经完成
//test1(f);
boolean b = f.cancel(true);
//System.out.println("取消任务执行的结果:"+b);
//Integer v = f.get(1, TimeUnit.SECONDS);//由于等待时间过短,任务来不及执行完成,会报异常
//System.out.println("任务执行的结果是:"+v);
}
//正常测试流程
private static void test1(Future<Integer> f) throws InterruptedException, ExecutionException {
boolean done = f.isDone();
System.out.println("第一次判断任务是否完成:"+done);
boolean cancelled = f.isCancelled();
System.out.println("第一次判断任务是否取消:"+cancelled);
Integer v = f.get();//一直等待任务的执行,直到完成为止
System.out.println("任务执行的结果是:"+v);
boolean done2 = f.isDone();
System.out.println("第二次判断任务是否完成:"+done2);
boolean cancelled2 = f.isCancelled();
System.out.println("第二次判断任务是否取消:"+cancelled2);
}
}
class MyCall implements Callable<Integer>{
private int a;
private int b;
//通过构造方法传递两个参数
public MyCall(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public Integer call() throws Exception {
String name = Thread.currentThread().getName();
System.out.println(name+"准备开始计算...");
Thread.sleep(2000);
System.out.println(name+"计算完成...");
return a+b;
}
}