https://mp.weixin.qq.com/s/baYuX8aCwQ9PP6k7TDl2Ww
Java线程池实现原理及其在美团业务中的实践 - 美团技术团队
上面两个链接都是一篇文章发布在不同地方,看其中一篇即可。
看到了美团技术团队的这篇文章以及加上自己对线程池的了解和看过的源码,有如下思考
对于线程池参数
对于线程池参数 corePoolSize 和 maximumPoolSize,业界没有一个很好的说法,但是目前一个比较好的做法如下
看当前线程池中要执行的任务是属于I/O密集型还是CPU密集型。
I/O密集型:线程频繁需要和磁盘或者远程网络通信,这种场景中磁盘的耗时和网络通信的耗时较大,意味着线程处于阻塞期间,不会占用CPU资源,所以线程数量设置超过CPU核心数并不会造成问题。
CPU密集型(计算型):对CPU利用率较高的场景,比如循环、递归、逻辑运算等,这种情况下线程数量设置越少,越能减少CPU的上下文频繁切换。
有一种建议如下,其中N表示CPU的核心数量。
- I/O密集型,线程池大小设置为 2N+1。
- CPU密集型,线程池大小设置为 N+1。
之所以需要+1,因为这样设置以后,线程在某个时刻发生一个页错误或者因为其他原因暂停时,刚好有一个额外的线程可以确保CPU周期不会中断。
对于CPU密集型,写了几个例子进行测试
测试机器为 intel i7 8 核,用一个幂运算来验证一下
public class ThreadTest {
public static void main(String[] args) {
int no = 10_000;
long start = System.currentTimeMillis();
for (int i = 0; i < no; i++) {
Math.pow(2, 32);
}
long duration = System.currentTimeMillis() - start;
System.out.println(duration);
}
}
1万数据计算结果用时响应结果0
public class ThreadTest {
public static void main(String[] args) {
int no = 1_000_000;
long start = System.currentTimeMillis();
for (int i = 0; i < no; i++) {
Math.pow(2, 32);
}
long duration = System.currentTimeMillis() - start;
System.out.println(duration);
}
}
100万数据计算结果用时响应结果5
public class ThreadTest {
public static void main(String[] args) {
int no = 1_000_000_000;
long start = System.currentTimeMillis();
for (int i = 0; i < no; i++) {
Math.pow(2, 32);
}
long duration = System.currentTimeMillis() - start;
System.out.println(duration);
}
}
1亿数据计算结果用时响应结果5
由此可知,计算型任务用时很少。用时基本卡在 io 相关的操作上。
对于任务提交策略
快速响应用户请求
对于快速响应用户请求这种场景,除了调大 corePoolSize 和 maximumPoolSize,还需要修改默认的线程池策略。让我想到了 tomcat 的线程池策略。具体相关可以看下面本人之前写的文章。
https://blog.csdn.net/zlpzlpzyd/article/details/131992445
java ThreadPoolExecutor 默认策略如下
corePoolSize -> 队列 -> maximumPoolSize -> 拒绝策略
这样有一个问题,如果需要马上执行的任务进队列阻塞不好,尤其是队列长度没设置的情况下,搞不好会内存溢出(尴尬)
tomcat ThreadPoolExecutor 策略如下
corePoolSize -> maximumPoolSize -> 队列 -> 拒绝策略
具体可以参照 tomcat 的线程池 org.apache.tomcat.util.threads.ThreadPoolExecutor,这个就是一个很好的案例
快速处理批量任务
这种场景对于资源有消耗,但是对于时间要求没那么高,可以把队列设置大一些,慢慢处理。
线程池监控
目前想到的办法如下
设置一个定时任务,任务执行后每秒扫描 spring ioc 容器中所有注入的 ThreadPoolExecutor 实例,通过名称进行区分(执行过程可能不会有一个,通过名称尽心区分,看日志时也知道哪个线程池出了问题),通过 toString() 中打印的各个变量获取对应的 get 方法,封装为一个java类,将对应的信息插入 elasticsearch,通过 prometheus 进行线程池各个参数实时状态分析。
线程池参数动态调整
类似于美团的做法,通过页面配置的方式调整对应的线程池,需要清楚执行的任务对时间的容忍度考虑对应的线程池策略选择对应的线程池。
页面配置参数提交后后台通过观察者模式触发监听器来修改 spring ioc 容器中对应名称的ThreadPoolExecutor 的 corePoolSize 和 maximumPoolSize 参数。