线程池使用及优势

上周我看到了一组主题。

主要任务是控制线程数量。

任务已排队。

按摩开始任务。

线路超线程。

特点:线程复用、并发控制和线程管理。

优点:效率高、资源少、管理好。

Java使用Executor框架。

执行器、执行器、ExecutorService、ThreadPoolExecutor。

执行者是工厂。

提供快速创作。
常用的线程池:FixedThreadPool、CachedThreadPool、SingleThreadExecutor。

查看源码:ThreadPoolExecutor创建完成。

构造方法有7 个参数。

工作流程:完整队列、完整序列和新任务被拒绝。

有四种拒绝策略。

ThreadPoolExecutor 实现 RejectedExecutionHandler。

线程池工作流程为什么是corepoolsize-->blockqueue-->maxp

说实话,调整线程池 corePoolSize 第一次遇到的时候让我很头疼。
想一想,生产环境中的任务如何能够完全区分是纯计算还是纯阻塞呢?我以前在电子商务项目中遇到过这种情况。
在后台处理订单的线程池中,当高峰期订单并发数增加时,CPU占用率上升到9 0%。
此时你一咬牙说corePoolSize设置太大了,结果半夜系统突然挂了:查看日志,原来是罕见的内存泄漏,任务卡在队列里,新任务进不去。
这堂课让我认识到,设置 corePoolSize 确实要靠经验和数据监控。

有趣的是,我们团队随后进行了统计:发现大约 6 0% 的任务是阻塞 I/O 的,剩下的 4 0% 是 CPU 密集型的。
根据这份报告,我们重新调整了线程池策略:纯阻塞任务直接进入一个线程池,CPU任务进入另一个线程池。
结果呢?资源使用量增加了近 2 0%,线程切换开销也低得多。
这个例子说明 corePoolSize 并不是万能的,需要根据任务的性质进行调整。

但是话虽如此,线程池设计中最有问题的还是如何动态调整。
我看到了一个开源监控系统。
他们开发了一种算法来实时统计线程的队列长度和空闲率。
如果队列长度超过阈值,则扩容,如果空闲线程数超过核心数,则减少容量。
老实说,这个算法已经存在了一段时间,我发现它的影响既有积极的也有消极的。
有时扩张太快,导致上下文切换混乱,有时收缩太快,无法响应。
我个人没有在这方面运行过完整的分布式环境,但我记得数据在X左右,但我建议你检查一下。

Java线程池有一个特性。
一旦 corePoolSize 和工作队列已满,新任务将创建新线程,最多可达 maxPoolSize。
这个时候效率其实已经不符合估计了,因为系统资源已经支撑不了了。
我以前在高并发系统中见过这种情况。
maxPoolSize 值设置得太小。
结果,当闪购事件发生时,系统的CPU就爆炸了。
当时我们白天改了线程池参数,晚上上线看效果。
如果结果成功我们会很高兴,但如果结果失败我们就会头疼。

现在看来,最好的办法就是提高活动的凝聚力。
例如,使用Go语言,创建协程的成本不到线程的1 %,并且可以毫无问题地执行数百或数千个协程。
我在一个微服务项目中尝试过这一点,并使用 Kotlin 协程来重建一些异步处理。
代码量减少了一半,性能也得到了提升。
当然,Java也有CompletableFuture,但是说实话,如果你用惯了Java再看Go协程,你会感觉思路有点不一样。

也就是说,Java线程池也有它的优点,例如它可以更好地与解决方案配合使用现有 JVM 优化。
最近在优化一个旧系统,发现直接使用Go协程虽然速度更快,但是JVM内存模型混乱,导致系统不太稳定。
所以这个事情没有绝对最优的方案,要看具体情况。