一、什么是线程池?
线程池是一种多线程处理形式,它预先创建一组线程并维护他们,以便在需要执行异步任务时可以立即使用,而不必每次都创建新线程,可以资源复用;
二、为什么要使用线程池?
-
减少重复创建与销毁线程,复用已有线程;(这也是池化技术出现的原因,比如数据库连接池,线程池等,都是为了避免重复创建与复用)
-
提高响应速度,任务到达时线程已经存在,无需等待线程的创建;
-
可以控制并发线程的数量,提高线程可管理性;
-
避免无限制创建线程从而导致系统崩溃,防止资源耗尽;(不使用线程池,频繁的创建线程可能会导致程序的崩溃,即便是在程序中偶尔创建1~2个线程,当该任务的并发量特变大的时候也可能会造成系统的崩溃,所以说使用线程池是一种更合理和更高效的选择)
三、线程池的具体应用
1、创建ThreadPoolExcutor实例,并指定相关参数;
ThreadPoolExcutor tpe = new ThreadPoolExcutor(
corePoolSize, //①核心线程数,即便处于闲置状态也不会被回收;
maximumPoolSize, //②线程池中允许的最大的线程数(核心线程+临时线程)
keepAliveTime, //③非核心线程存活时间,非核心线程处于空闲超过指定时间则被回收;
keepAliveTimeUnit, //④keepAliveTime的时间单位TimeUnit.SECONDS、MINUTES等;
workQueue, //⑤线程池中用于存储任务的队列,有无界队列、有界队列等;
线程工厂, //⑥用于创建线程的工程类,一般使用默认的即可;
拒绝策略) //⑦当没有空闲的线程,且任务队列满后采用的策略;
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
10,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1024), //有界队列,大小为1024;
Executors.defaultThreadFactory(), //默认工厂;
new ThreadPoolExecutor.AbortPolicy() //直接抛出异常;
);
2、线程池的拒绝策略
当(核心线程 + 临时线程) >= 最大核心线程池数,且任务队列也满后,再次提交任务时,触发的策略,被称为拒绝策略;线程池中有默认的策略供我们选择,也可自定义;
1. AbortPolicy:直接抛出异常,默认策略 【能让开发人员及时发现问题,并及时解决】
2. DiscardPolicy:丢弃任务,不处理 【无法发现系统问题,可能会导致一些问题无法解决,建议在一些无关紧要的任务中使用】
3. DiscardOldestPolicy:丢弃队列中最旧的任务,尝试执行新任务【会直接丢弃,建议在一些无关紧要的任务中使用】
4. CallerRunsPolicy:由调用者线程来执行任务【比如是main调用的,那就由main线程来执行,不会导致任务丢失】
5. 自定义策略,实现RejectedExecutionHandler接口,并实现rejectedExecution()方法,在该方法中编写自定义拒绝策略的逻辑;
3、利用线程池执行任务;
① ThreadPoolExecutor实例.execute(任务)【无返回值】
② ThreadPoolExecutor实例.submit(任务) 【有返回值,返回Future对象,可获取结果】
示例①
threadPoolExecutor.execute(() -> {
System.out.println("我是通过ThreadPoolExecutor创建的线程!");
//....任务代码
});示例②
Future<String> submit = threadPoolExecutor.submit(() -> {
System.out.println("我是通过ThreadPoolExecutor创建的线程!");
//....任务代码
return "返回值";
});try {
submit.get(); // 获取线程执行结果;
} catch (Exception e) {
throw new RuntimeException(e);
}
4、线程池使用补充
① 默认情况下,核心线程不会被回收,如果需要回收核心线程,通过以下配置后,即便是核心线程处于空闲状态也会被回收:
- threadPoolExecutor.allowCoreThreadTimeOut(true);
② 默认情况下,创建线程池后不会马上自动创建线程,而是需要提交任务后才会创建线程;如果想创建线程池时就创建线程,可以使用以下两种方式:
- threadPoolExecutor.prestartCoreThread();(创建线程池同时创建一个核心线程)
- threadPoolExecutor.prestartAllCoreThreads();(创建线程池同时创建好所有核心线程)
③ 如何正确的关闭线程池
threadPoolExecutor.shutdownNow():停止接收新的任务,将正在执行的任务中断,忽略队列里面待执行的任务,返回没有执行的任务集合;
threadPoolExecutor.shutdown():停止接受新的任务,内部正在执行的任务和队列里面的任务会继续执行直到执行完毕,等待所有任务都执行完后才会关闭线程池;
threadPoolExecutor.awaitTermination(时间,单位):判断线程池是否关闭,已关闭返回true,否则阻塞传入的指定时间再次判断是否关闭;
优雅关闭线程池示例👇:
executor.shutdown(); //1、关闭线程池;
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { //2、判断是否关闭成功;
executor.shutdownNow(); //3、超过时间未关闭成功,强制关闭;
}
④ 线程池的扩展方法
自定义一个类继承ThreadPoolExecutor,然后实现里面的方法:
beforeExecute():线程任务执行前调用的方法,可以在此编写需要执行的业务逻辑;
afterExecute():任务执行结束后调用的方法,可以在此编写需要执行的业务逻辑;
terminated():线程池完全终止后调用的方法,可以在此编写需要执行的业务逻辑;
四、线程池的工作原理
①首先,创建线程池并指定相关参数;②然后当我们使用execute或submit方法向线程池提交任务;③如果线程池中有核心线程则直接分配线程去执行任务,否则会创建核心线程;④直到达到最大核心线程数量,再次提交的任务会被放到任务队列里面;⑤当核心线程没有空闲的、任务队列也已经满了之后,则会创建临时线程去执行异步任务;⑥当临时线程 + 核心线程数达到最大线程数量,且任务队列也已经满了之后,再次提交的任务就会触发拒绝策略;如图:
五、补充
1、合理设置线程池大小
- 公式 ①:CPU的核心数 * CPU的使用率(0% - 100%) * (1 + (线程等待时间(60%)/ 线程计算时间(40%)))
- 公式 ②:CPU的核心数 * (1 - 阻塞系数)
【其中阻塞系数为0.1 ~ 0.9,表示有多少时间是处于等待状态】比如0.4表示有40%的时间处于等待,比如IO等待,网络请求等待,60%的时间是CPU真正的在运行,根据任务的具体业务来判断;
根据以上两个公式,国内也采用了一些方案,在Java代码中可以使用:获取该服务器的可用核心线程数量,方便我们动态的获取然后创建;
Java代码中获取服务器核心线程数:Runtime.getRuntime().availableProcessors();
- 1、CPU密集型任务:线程数 = CPU核心线程数 + 1
- 2、I/O密集型任务:线程数 = CPU核心线程数 * 2
- 3、其他通用型:线程数 = ((线程等待时间 + 线程CPU运行时间)/ 线程CPU运行时间)* CPU核心数量
2、正确创建线程池
不要在被经常调用的方法、多列类里创建线程池;
原因:因为线程池里面的核心线程默认不会被销毁,当频繁创建线程池会内积占用内存资源,最终导致内存溢出;
① 不在方法里创建线程池,而是作为类的成员或静态变量,但也要显式的调用shutdown(),否则类创建的频率高时也会出现内存溢出的问题;
优点:"特定场景",资源隔离,可以避免全局竞争,关键的业务独享一个线程池,可以根据不同的场景创建不同的线程池;
② 使用Spring的@Bean注解将线程池实例交给IOC容器管理,全局共享线程池,需要使用时注入即可;
优点:"通用场景",适用于全局任务,简单异步处理,创建后不需要手工维护(也可以根据不同业务配置多个线程池,都交给IOC容器管理)
注:如果非要在方法里面创建,可以将核心线程设置为可以被销毁,或使用后将线程池的资源释放也可以解决这样的问题;(不推荐)
3、创建线程池阿里规范
不能使用Executors工具类去创建线程池,因为它允许的队列长度、线程数量过大,且不能改变;可能导致OOM;而是通过new ThreadPoolExecutor()并指明相关参数的方式去创建,控制核心线程数,最大线程数,队列数量,拒绝策略等信息;
4、线程池的监控
应用场景:对线程池的特定参数进行监控,将这些数据持久化。当程序运行过程中出现问题时,便于我们去排查和定位问题;
实现方式:定时采集线程池运行时的数据,并将这些数据持久化,便于后续查看然后定位问题;
具体实现:
① 创建一张用于存储线程池运行时所产生的数据;
② 编写定时任务,定时收集线程运行相关数据,然后存入数据库里;
注:详细示例可以自行查阅;