本文由杨青同学投稿, 总结了他在近期工作中对线程池容量设置的一点经验原文发于微信公众号: Java 线程池容量设置
创建线程池的方式
Java 中可以通过 Executors 和 ThreadPoolExecutor 的方式创建线程池, 通过 Executors 可以快速创建四种常见的线程池, 但这种方式在实际使用中并不推荐, 因为这种方式创建出来的线程池可控性较差, 更推荐的方式是使用 ThreadPoolExecutor 提供的方法参考阿里巴巴 Java 开发规范:
强制线程池不允许使用 Executors 去创建, 而是通过 ThreadPoolExecutor 的方式, 这样的处理方式让写的同学更明确线程池的运行规则, 规避资源耗尽的风险
说明: Executors 返回的线程池对象弊端如下:
1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE, 可能会堆积大量的请求, 从而导致 OOM
2)CacheThreadPool 和 ScheduledThreadPool: 允许创建线程数量为 Integer.MAX_VALUE, 可能会创建大量线程, 从而导致 OOM
参数的工作原理
使用 ThreadPoolExecutor 创建线程池需要自己给出关键的参数:
- corePoolSize:
- maximumPoolSize
- keepAliveTime
- unit
- workQueue
- threadFactory
- handler
这几大参数的工作原理如下:
图 1 选自 infoQ 文章聊聊并发(三)
具体讲解可以参考清英在 infoQ 发布的文章: http://www.infoq.com/cn/articles/java-threadPool
线程池容量设置
线程池的 corePoolSize 设置是整个线程池中最关键的参数, 设置太小会导致线程池的吞吐量不足, 因为新提交的任务需要排队或者被 handler 处理掉(取决于拒绝策略); 设置太大可能会耗尽计算机的 CPU 和内存资源
CPU bound
在 CS 中, CPU 密集型任务的执行时间主要取决于 CPU 的时间 CPU 密集型任务的线程数通常设置为 CPU 核心数 + 1 ,Java Concurrency in Practice(由 Doug. Lea 等人撰写)给出了如下的解释:
Even computeͲintensive threads occasionally take a page fault or pause for some other reason, so an
"extra" runnable thread prevents CPU cycles from going unused when this happens.
即比 CPU 核心数多出来的一个线程是为了防止线程偶发的 page fault 或者由于其他原因导致的任务暂停, 此时 CPU 就会处于空闲状态, 而在这种情况下多出来的一个线程就可以充分利用 CPU 的这个空闲时间
IO bound
IO 密集型任务是指任务的执行时间主要取决于 IO 的时间在笔者实习期间遇到的业务场景中大部分属于 IO bound 而对于 IO bound, 书中则给出了如下的计算公式:
最佳线程数 = CPU 数量 * CPU 利用率 *(线程等待时间 / 线程 CPU 时间 + 1)
实际最优参数
在实际操作中, 由于公式中线程等待时间和线程 CPU 时间不好估算, 以及系统中存在其他的阻塞情况, 这样计算出来的最佳线程数往往不是生产环境下的最佳线程数为了提高精确度, 笔者利用有赞内部的压测系统对代码进行压测, 通过多次调整计算出来的最佳线程数和观察压测结果 (系统负载接口 RTTPS 等指标) 来判断线程数是否符合期望
在压测的过程中发现, 当线程数量设置的更合理时 TPS 更高且接口的 RT 较低; 而线程池设置过大导致 TPS 下降和 RT 上涨由于 RT 和 TPS 不太方便直接给出, 这里仅展示系统负载这一指标的压测结果
当线程池设置过大时:
图 2 压测时把线程池参数设置得很大
当线程池设置较为合理时:
图 3 线程数较为合理
参考资料:
- http://www.infoq.com/cn/articles/java-threadPool
- https://en.wikipedia.org/wiki/I/O_bound
- https://en.wikipedia.org/wiki/CPU-bound
- Java Concurrency in Practice
来源: http://www.jianshu.com/p/47b09eafab37