为什么使用线程池,线程池有哪些优势?
线程池能控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务。如果任务数量超过最大线程数,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务执行。
特点: 线程复用,控制最大并发数,管理线程
优势:
- 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不用等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的工作原理
- 在创建线程池后,等待提交过来的任务请求。
- 当调用execute()方法添加一个请求任务时,线程池会做如下判断:
如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
如果队列满了且正在运行的线程数还小于maximumPoolSize,那么会创建非核心线程立即运行这个任务;
如果队列满足且正在运行的线程数大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略执行。
- 当一个线程完成任务时,从队列中取出下一个任务来执行。
- 当一个线程空闲超过一定的时间(keepAliveTime),线程池会判断,如果当前运行的线程大于corePoolSize,那么这个线程会被停掉
ThreadPoolExecutor的7大核心参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
- corePoolSize:线程池中核心线程数
- maximumPoolSize:线程池中能容纳同时执行的最大线程数,必须大于等于1
- keepAliveTime:多余空闲线程的存活时间
- unit:keepAliveTime的时间单位
- workQueue:阻塞队列,用于存储被提交但还未被执行的任务
- threadFactory:生成线程池中工作线程的线程工厂,用于创建线程,一般默认即可
- handler:拒绝策略,当阻塞队列满了且工作线程大于线程池的最大线程数时,如何拒绝任务
案例
核心线程数2,最大线程数5,阻塞队列可容纳3任务,采用CallerRunsPolicy拒绝策略
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 1,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
1)task = [2,5],核心线程就可以完成所有任务的执行
try {
int task = 5;
for (int i = 1; i <= task; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName() + " 执行业务");
});
}
}catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
结果分析:前2个任务核心线程处理,后3个任务进入阻塞队列,最后再由核心线程处理
pool-1-thread-1 执行业务
pool-1-thread-2 执行业务
pool-1-thread-1 执行业务
pool-1-thread-2 执行业务
pool-1-thread-1 执行业务
2)task = [6,8],除了核心线程外,还需要创建新的线程才能完成所有任务的执行
try {
int task = 8;
for (int i = 1; i <= task; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName() + " 执行业务");
});
}
}catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
结果分析:thread-3,4,5都是核心线程数之外的线程。核心线程数满了,阻塞队列也满了,只能创建新的线程处理任务
pool-1-thread-1 执行业务
pool-1-thread-5 执行业务
pool-1-thread-1 执行业务
pool-1-thread-4 执行业务
pool-1-thread-3 执行业务
pool-1-thread-2 执行业务
pool-1-thread-1 执行业务
pool-1-thread-5 执行业务
3)task > 8,核心线程数满了,阻塞队列也满了,最大线程数也满了,此时后续的任务都由调用者执行,因为线程池采用CallerRunsPolicy拒绝策略
try {
int task = 9;
for (int i = 1; i <= task; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName() + " 执行业务");
});
}
}catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
结果分析:main线程也参与任务的执行
pool-1-thread-1 执行业务
pool-1-thread-4 执行业务
main 执行业务
pool-1-thread-3 执行业务
pool-1-thread-2 执行业务
pool-1-thread-3 执行业务
pool-1-thread-4 执行业务
pool-1-thread-1 执行业务
pool-1-thread-5 执行业务
线程池的最大线程数如何设置?
- CPU密集型:任务需要大量的运算,没有阻塞,CPU一直全速运行。
CPU密集型的任务只有在多核CPU上才可能通过多线程加速,在单核CPU上开启多线程,上下文切换的开销大。
CPU密集型任务配置尽可能少的线程数量,一般公式:CPU核数+1
- IO密集型:IO密集型的任务线程并不是一直执行的,应配置尽可能多的线程。IO密集型的任务需要大量的IO,即大量阻塞,在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力,因此需要多配置线程数
参考公式:CPU和数/1 + 阻塞系数(0.8-0.9之间)
比如8核CPU:8/(1 - 0.9) = 80个线程数
获取CPU核数,可使用方法:Runtime.getRuntime().availableProcessors()
线程池的拒绝策略
- AbortPolicy(默认):直接抛RejectedExecutionException异常,阻止系统正常运行
- CallerRunsPolicy 由调用者自己运行任务
- DiscardOldestPolicy 丢弃阻塞队列中最老的任务并将新任务加入队列
- DiscardPolicy 直接丢弃,不做任何处理