本文是向大家介绍线程池的使用和一些注意事项,它能够实现高并发下快速处理业务,能够帮助开发人员深入理解线程池的价值。
1. 简介
线程池是使用池化技术管理和使用线程的一种机制。池化技术:提前准备一些资源,在需要时可以重复使用使用提前准备的资源。常见的有内存池、数据库连接池等。
2. 参数说明
- 需要如下参数:
corePoolSize:核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
maximumPoolSize:线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
keepAliveTime:线程闲置超时时长。如果超过该时长,非核心线程就会被回收。
unit:指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
workQueue:任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
- 参数配置异常
if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize || keepAliveTime < 0){ throw new IllegalArgumentException(); }
3. 使用顺序
核心线程满->任务队列满->最大线程满->拒绝处理
一个线程池所能承载最大线程量:任务队列大小+最大线程数
4. 默认线程工厂
Executors 框架已经为我们实现了默认的线程池:
线程池 | 最大线程数 | 任务队列 | 应用场景 |
FixedThreadPool | 自定义 | LinkedBlockingQueue | 控制线程最大并发数,cpu比较平稳 |
ScheduledThreadPool | Integer.MAX_VALUE | DelayedWorkQueue | 执行定时或周期性的任务 |
CachedThreadPool | Integer.MAX_VALUE | SynchronousQueue | 执行大量、耗时少的任务 |
SingleThreadExecutor | 1 | LinkedBlockingQueue | 单线程的线程池,保证执行顺序 |
总结:
- FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,而LinkedBlockingQueue未指定容量,默认大小是Integer.MAX_VALUE,可能会耗费非常大的内存导致 OOM。任务队列无界时,设置最大线程数是没有意义的。
- CachedThreadPool 和 ScheduledThreadPool:主要问题是线程最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程导致 OOM。
- Executors 的 4 个功能线程池虽然方便,但不建议使用,而是建议直接通过使用 ThreadPoolExecutor 的方式,规避资源耗尽的风险
5. 正确打开方式
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( //核心线程数 1, //最大线程数 1, //超时时间 1L, //时间单位 TimeUnit.MILLISECONDS, //任务队列 new LinkedBlockingDeque<Runnable>(1), //线程工厂 new MyThreadFactory(), //拒绝策略 new MyThreadRunsPolicy());
- 指定最大线程数
- 任务队列指定大小
- 线程工厂自定义
参考DefaultThreadFactory,自定义MyThreadFactory只修改了名字前缀,方便查询日志
public class MyThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; MyThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); //此处只修改了一下名字前缀 namePrefix = "业务1-" + poolNumber.getAndIncrement() + "-thread-"; System.out.println("--->"+namePrefix); } @Override public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()){ t.setDaemon(false); } if (t.getPriority() != Thread.NORM_PRIORITY){ t.setPriority(Thread.NORM_PRIORITY); } return t; } }
- 自定义拒绝策略
public class MyThreadRunsPolicy implements RejectedExecutionHandler { MyThreadRunsPolicy() { super(); } @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { try { final Thread t = new Thread(r, "Temporary task executor"); t.start(); } catch (Throwable e) { throw new RejectedExecutionException("Failed to start a new thread", e); } } }
Executors 框架已经为我们实现了 4 种拒绝策略:
- AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
- CallerRunsPolicy:由调用线程处理该任务,
- DiscardPolicy:丢弃任务,但是不抛出异常。
- DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
6. springboot集成线程池
- 全局线程池配置
@Configuration @EnableAsync // 利用@EnableAsync注解开启异步任务支持 public class CustomMultiThreadingConfig { @Bean("threadPoolTaskExecutor") public Executor getAsyncExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); // 核心线程数 taskExecutor.setCorePoolSize(10); // 最大线程数 taskExecutor.setMaxPoolSize(20); //配置队列大小 taskExecutor.setQueueCapacity(10); // 配置线程池前缀 taskExecutor.setThreadNamePrefix("async-service-"); // 配置拒绝策略 taskExecutor.setRejectedExecutionHandler(new MyThreadRunsPolicy()); // 数据初始化 taskExecutor.initialize(); return taskExecutor; } private static final class MyThreadRunsPolicy implements RejectedExecutionHandler { MyThreadRunsPolicy() { super(); } @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { try { final Thread t = new Thread(r, "Temporary task executor"); t.start(); } catch (Throwable e) { throw new RejectedExecutionException("Failed to start a new thread", e); } } } }
- 待线程池执行任务添加@Async("threadPoolTaskExecutor")
@Component public class AsyncSubTaskExecutorServiceImpl implements AsyncSubTaskExecutorService { @Async("threadPoolTaskExecutor") @Override public void extracted() { } }
注意:异步方法A调用异步方法B时,A和B不能在同一类中,否则无效
7. 浅谈设置线程池大小
7.1 计算线程池最优大小的公式:
W/C 称为阻塞系数,对于CPU密集型任务阻塞系数为0,cpu核的数量就是线程数,拥有更多的线程数也是用处不大的。也可以得到“线程等待时间所占比例越高,需要越多线程”。
7.1.1 获取核数
16个CPU,1核,所以有16个逻辑CPU
7.1.2 获取等待输入输出的CPU时间百分比
"top"命令查看cup详情
按“1”键查看所有cpu
wa 等待输入输出的CPU时间百分比,CPU等待磁盘IO操作的时间
us 用户空间占用CPU百分比,比如shell程序、各种语言的编译器、数据库应用、web服务器和各种桌面应用等;
sy 内核空间占用CPU百分比,系统资源都是由Linux内核处理,比如分配一些内存、或是执行IO操作、再或者是去创建一个子进程。
7.2 实战:reconciliationweb结算同步任务线程数调试
1. 根据并发高低和任务执行时间来估算线程数
并发 | 任务执行时间 | 业务类型 | 估算 |
高 | 短 | cpu核数+1,减少线程上下文切换 | |
高 | 长 | 使用中间件等加速执行时间 | |
低 | 长 | IO密集型: 文件传输,网络请求等,cpu需等待结果后执行下一步 | cpu核数*2+1 |
CPU密集型: 递归计算等 | cpu核数+1 | ||
低 | 短 | cpu核数+1 |
通过linux命令查看reconciliationweb服务核数为16,结算同步业务仅商务人员操作,并发低,但数据量大,数据量达400万,调用支付宝接口执行时间长,属于IO密集型,那么线程数起点可以配置33来调试。
2. 查看reconciliationweb服务分配核数
查看服务分配2核cpu,8g内存;
3. k8s上查看cpu使用情况
通过不断调试核数,来查看cpu使用的变化趋势,在满足平均cpu使用率不超过80%的情况下找到接近最优解的值;