目录
一、背景
为了实现异步,需要将任务开新的线程去处理调三方接口等,从而不影响主线程的工作。而配置线程池可以方便线程的管理,减少线程创建、摧毁带来的性能消耗和提高响应速度等。这篇文章将简单介绍如何配置线程池。
二、什么是CPU核数和逻辑处理器数
ctrl+alt+delete打开任务管理器,打开性能,如下图我的机器配置,可以发现右下角有内核和逻辑处理器
内核指的就是CPU核数,逻辑处理器是线程数-最大可以并行的线程数(这里我搜索了无数的资料,验证了CPU核数和上述的内核是不同的描述而已,它两相等。如果还错,那真打脸了..)
有了这个知识,下面还要简单介绍一下CPU密集型和IO密集型
三、CPU密集型和IO密集型
线程任务可以分为CPU密集型和IO密集型。(平时开发基本上都是IO密集型任务)
CPU密集型任务的特点是进行大量的计算,消耗CPU资源,比如计算圆周率、视频高清解码等。这种任务操作都是比较耗时间的操作,任务越多花在任务切换的时间就越多,CPU执行任务效率就越低。所以,应当减少线程的数量,CPU密集型任务同时进行的数量应当等于CPU的核心数,上述我的电脑为8。
IO密集型的任务的特点是涉及到网络(调用三方接口)、磁盘IO(文件操作)等。这类任务操作是CPU消耗很少,任务大部分时间都在等待IO操作完成(IO的速度远低于CPU和内存的速度)。对于这种任务,任务越多,CPU效率越高,但是也有限度。我们开发接口时,像调别的应用接口,基本逻辑处理等,基本上都是属于IO密集型任务。
刚才提到了,CPU密集型尽量配置少的线程,核心线程配置:CPU核数。而IO线程池应配置多的线程,核心线程配置:CPU核数*2。这里IO密集型还有一种情况是线程易阻塞型的,需要计算阻塞系数,他是这么配置线程核心线程数的:CPU核数 / 1 – 阻塞系数(0.8~0.9之间)。
四、线程池核心线程数确定
Java给我们提供了获取逻辑处理器-也就是最大并行可以执行的线程数的API,如下图我的机器为16
核心线程计算:根据第三节的知识,及我的第一节应用背景是异步调三方接口,属于IO密集型,核心线程配置:CPU核数*2=8*2=16。(这里不考虑另一种情况配置)
其实API直接计算得到的值就是16,刚好满足,下面是实现代码,用的是ThreadPoolTaskExecutor,底层封装了ThreadPoolExecutor。后面专门写篇文章介绍一下。
五、代码实现-ThreadPoolTaskExecutor
其他参数不了解的可以参考我之前的文章介绍:超详细的线程池介绍。
ThreadPoolTaskExecutor类不了解的或者它与ThreadPoolExecutor的区别参考另一篇文章:ThreadPoolTaskExecutor的点滴理解
线程池配置代码:(这里是通过Bean的方式)
@Component
public class ThreadPoolConfig {
/**
* 线程池名称
*/
private static final String TASK_THREADPOOL_NAME = "task_threadpool_test";
/**
* 线程池配置
* @return 线程池
*/
@Bean(TASK_THREADPOOL_NAME)
public Executor asyncThreadPoolExecutor() {
// 获得运行机器 逻辑处理器核数(其实就是线程数)
Integer availibleNum = Runtime.getRuntime().availableProcessors();
// 内部还是使用了ThreadPoolExecutor,只是参数配置更加方便
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心参数
executor.setCorePoolSize(availibleNum);
// 配置最大线程数 线程数*2
executor.setMaxPoolSize(availibleNum * 2);
// 配置队列容量 队列:不配置容量,用SynchronousQueue,配置用LinkedBlockingQueue。 (BlockingQueue)(queueCapacity > 0 ? new LinkedBlockingQueue(queueCapacity) : new SynchronousQueue());
executor.setQueueCapacity(100);
// 配置非核心线程超时释放时间(核心线程默认不允许回收)
executor.setKeepAliveSeconds(60);
// 配置线程名称前缀
executor.setThreadNamePrefix(TASK_THREADPOOL_NAME);
// 配置拒绝策略,CallerRunsPolicy:当拒绝时由调用线程处理该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
线程池使用代码:
@Component
public class MsgTaskUtils {
@Autowired
@Qualifier("task_threadpool_test")
private Executor asyncExecutor;
public void dealTask() {
// 模拟任务
Runnable runnable = new Runnable() {
@Override
public void run() {
}
};
// 通过线程池方式执行
asyncExecutor.execute(runnable);
}
}
如果不用Bean的方式如何实现?代码如下(需要自己调用initalize初始化线程池)
/**
* 自己new的,不会自己初始化,需要调用initialize才能用,否则报错
* java.lang.IllegalStateException: ThreadPoolTaskExecutor not initialized
*/
public static ThreadPoolTaskExecutor asyncThreadPoolExecutor() {
// 获得运行机器 逻辑处理器核数(其实就是线程数)
Integer availibleNum = Runtime.getRuntime().availableProcessors();
// 内部还是使用了ThreadPoolExecutor,只是参数配置更加方便
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心参数
executor.setCorePoolSize(availibleNum);
// 配置最大线程数
executor.setMaxPoolSize(availibleNum * 2);
// 配置队列容量 队列:不配置容量,用SynchronousQueue,配置用LinkedBlockingQueue。 (BlockingQueue)(queueCapacity > 0 ? new LinkedBlockingQueue(queueCapacity) : new SynchronousQueue());
executor.setQueueCapacity(100);
// 配置非核心线程超时释放时间(核心线程默认不允许回收)
executor.setKeepAliveSeconds(60);
// 配置线程名称前缀
executor.setThreadNamePrefix(TASK_THREADPOOL_NAME);
// 配置拒绝策略,CallerRunsPolicy:当拒绝时由调用线程处理该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 不用bean注入方法,需要自己初始化线程池
executor.initialize();
return executor;
}
注意点:如果不是通过Bean的方式实现,那么需要手动去调用initialize方法去初始化线程池,因为ThreadPoolTaskExecutor-Bean实现了InitializingBean接口的Bean会调用,其里面的afterPropertiesSet去初始化线程池。
线程池核心线程数配置到此已经写完,有啥问题可以留言探讨。