1.线程池简介
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了线程池的最大数量,则超出部分的线程在等候区排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
他的主要特点是:线程复用;控制最大并发数;管理线程。
他的主要优点有:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以拿取已创建的线程直接执行。
- 提高线程的可管理性:线程是稀缺资源如果无限制的创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以进行统一的分配,调优和监控。
2.线程池框架
线程池底层就是ThreadPoolExcutor类,而Exexutors就是用来创建、管理线程池的工具类,类似于数组工具类Arrays。
工具类创建线程池的3个核心方法:
- Executors.newFixedThreadPool(int n) : 一池n线程,执行长期的任务,性能优越
- Executors.newSingleThreadExecutor() : 一池一线程,一个任务一个任务执行的场景
- Executors.newCachedThreadPool() : 一池多线程可扩容,执行大批量且短期的异步任务
前三种获取线程的方式可通过右边链接回顾:
读清手动方式创建线程的优缺点
3.第四种获得java多线程的方式:线程池演示
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 一池3个处理线程
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 模拟10个待办任务,都交给线程池的execute()执行
try {
for (int i = 1; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行了任务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
4.从Executors 线程池工具类3种创建线程的核心方法,获知底层都是实例化了ThreadPoolExecutor类,只是构造参数不同。
4.1. Executors.newFixedThreadPool(int n):
主要特点为:- 创建定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue阻塞队列。
4.2.Executors.newSingleThreadExecutor():
主要特点为:- 创建一个单线程的线程池,它只有一个工作线程执行任务,保证所有任务按照指定顺序执行。 - newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的是LinkeBlockingQueue阻塞队列。
4.3.Executors.newCachedThreadPool():
主要特点为:- 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newCachedThreadPool将corePoolSize设置为0,maximumPoolSize设置为最大Int值,它使用的是SynchronousQueue同步队列。所有线程执行待办任务,如果某个线程空闲超过60秒,则销毁该线程。
5.new ThreadPoolExecutor(…)线程池7大重要参数介绍
所有线程池底层的构造方法都是下面这个方法:
图解如下:
1.corePoolSize:线程池中的常驻核心线程数。
2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1。
3.keepAliveTime:多余的空闲线程的存活时间,当前线程池数量超过maximumPoolSize时,空闲时间达到keepAliveTime值的多余线程将会被销毁,直至剩下corePoolSize个线程为止。
4.unit:keepAliveTime的时间单位。
5.workQueue:任务队列,存放已提交但尚未被执行的任务。
6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般使用工厂的默认参数即可。
7.handler:拒绝策略,表示当任务队列满了,并且工作线程>=线程池的最大线程数(maximumPoolSize)时如何来拒绝新增的任务
7.线程池的工作原理
工作原理图解如下:
1.在创建了线程池后,等待提交过来的任务请求。
2.当调用executor()方法添加一个请求任务时,线程池会做如下判断:
2.1.如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行待办任务;
2.2.如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
2.3.如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4.如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3.当一个线程完成任务时,它会从队列中取出下一个任务来执行。
4.当一个线程无事可做超过一定时间(keepAliveTime)时,线程池会进行判断逻辑:如果当前在运行的线程数大于corePoolSize,那么这个空闲线程就是多余的线程,会被销毁掉。
8.JDK的4种线程池内置拒绝策略
拒绝现象发生的时机:阻塞队列满了,再也塞不下新的任务。同时线程池中的线程数已经达到了max,无法再次创建线程。
4种内置策略及其解释如下:
1.AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
2.CallerRunsPolicy:“调用者运行”是一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,由调用者线程执行该任务。
3.DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
4.DiscardPolicy:直接丢弃任务,不允任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
JDK默认拒绝策略如下:
9.生产环境中使用的线程池
在生产环境中Executors的3种直接创建线程池的方法都不宜使用,而是使用手写构造参数来自定义线程池。
理由如 阿里开发手册 所言:
9.1自定义线程池参数演示
由演示代码可知当前线程池的最大线程数为2,阻塞队列空间为2,并发待执行任务为10。从构造参数和并发数可知会导致线程池拒绝策略的执行,线程池的拒绝策略使用的是CallerRunsPolicy,即当并发任务数导致队列满,并大于max线程数时,之后到达的任务将会由main线程代为执行,结果如下:
9.2合理配置线程池的考虑
1.调用如下代码查看服务器的CPU核数
2.区分待执行任务的类型
- CPU密集型:是指该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
此时应该配置尽可能少的线程数量:CPU核数+1个线程的线程池。 - IO密集型:
1.IO密集型的任务线程并不是一直在执行任务,则应配置尽可能多的线程:CPU核数*2
2.该任务需要大量的IO,即大量的阻塞,故需要多配置线程数:CPU核数 / (1 - 阻塞系数)
比如8核CPU:8 / (1 - 0.9) = 80个线程数