ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java 自定义线程池

Java 自定义线程池 Java 自定义线程池1. 为什么要使用线程池如果每来一个任务就创建一个线程newThread(()-{// 执行任务}).start();大量请求过来以后可能变成1000 个请求 ↓ 创建 1000 个线程 ↓ 线程创建成本 线程上下文切换 内存占用 CPU 竞争 ↓ 系统性能下降线程池的核心思想是复用已经创建的线程限制并发线程数量。2. 自定义线程池Java 中最常见的方式ThreadPoolExecutorexecutornewThreadPoolExecutor(4,// 核心线程数8,// 最大线程数60,// 非核心线程空闲存活时间TimeUnit.SECONDS,newArrayBlockingQueue(100),// 工作队列newThreadPoolExecutor.CallerRunsPolicy()// 拒绝策略);线程池最重要的几个参数corePoolSize maximumPoolSize workQueue keepAliveTime RejectedExecutionHandler其中今天重点关注核心线程数 最大线程数 CPU 核数3. 核心线程数是什么假设corePoolSize4可以简单理解为线程池希望长期维持的基础工作线程数量。例如线程池 Thread-1 Thread-2 Thread-3 Thread-4正常情况下这 4 个线程负责处理任务。注意核心线程数不是“最多只能同时运行 4 个任务”。这是很多人容易误解的地方。因为还有最大线程数 工作队列共同决定线程池最终可以创建多少线程。4. 最大线程数是什么假设corePoolSize4;maximumPoolSize8;意味着正常 最多先使用 4 个核心线程 压力增大 可以继续创建线程 最多 8 个线程可以理解成最大 8 个线程 ┌──────────────────┐ │ │ │ 核心线程 4 个 │ │ │ │ 非核心线程 4 个 │ │ │ └──────────────────┘但是这里有一个非常重要的前提线程池不会因为任务增加就直接从 4 个线程扩张到 8 个线程。中间还有一个workQueue。5. ThreadPoolExecutor 提交任务的过程假设corePoolSize4maximumPoolSize8queue100提交任务时大致按照这个逻辑提交任务 ↓ 当前线程数 核心线程数 │ ├── 是 → 创建核心线程执行 │ └── 否 ↓ 放入队列 ↓ 队列满了吗 │ ├── 没满 → 等待线程处理 │ └── 满了 ↓ 当前线程数 最大线程数 │ ├── 是 → 创建非核心线程 │ └── 否 → 执行拒绝策略所以核心线程数 ↓ 工作队列 ↓ 最大线程数 ↓ 拒绝策略这个顺序非常重要。6. 一个具体例子假设corePoolSize4;maximumPoolSize8;queue100;突然来了 200 个任务。第 14 个任务创建核心线程4 个线程 ↓ 执行 4 个任务第 5104 个任务核心线程已经存在。剩余任务进入队列4 个线程 100 个队列任务第 105 个任务开始队列满了。这时候线程池才开始创建非核心线程4 个核心线程 最多 4 个非核心线程最终最多8 个线程 100 个排队任务如果 8 个线程都在工作同时队列又满了新任务 ↓ 拒绝策略7. 核心线程数和 CPU 核数有什么关系这是最容易被误解的地方。很多文章会直接说CPU 密集型 核心线程数 CPU 核心数 1 IO 密集型 核心线程数 CPU 核心数 × 2不要把这个当成固定公式。它只能作为一个非常粗略的起点。真正需要考虑的是CPU 核数 任务类型 任务执行时间 IO 等待时间 任务到达速度 队列长度 内存 上下游系统承载能力8. CPU 密集型任务例如大量计算 图片处理 压缩 加密 复杂数据计算任务特点大部分时间都在使用 CPU假设物理机8 核 CPU线程池100 个线程并不意味着100 个线程 → 100 个 CPU 核心同时计算实际上CPU ├── 核心 1 ├── 核心 2 ├── ... └── 核心 8 ↑ 100 个线程竞争同一时刻真正能够执行 CPU 指令的线程数量受到 CPU 核心数量限制。因此 CPU 密集型任务线程太少 → CPU 利用率上不去 线程太多 → 上下文切换增加 → CPU 浪费所以一般会让线程数接近 CPU 核数而不是无限增加。9. IO 密集型任务例如数据库查询 HTTP 请求 文件读取 RPC 网络请求线程可能经常处于执行代码 ↓ 等待 IO ↓ 执行代码 ↓ 等待 IO例如线程 1 → 等待数据库 线程 2 → 等待 HTTP 线程 3 → 等待 Redis 线程 4 → 等待文件虽然机器只有8 核 CPU但是并不代表只能创建 8 个线程。因为很多线程正在等待 IO。这时候可以使用比 CPU 核数更多的线程8 核 CPU 线程 16 24 32 ...具体多少需要根据实际任务测试。10. 一个重要区别物理核心 ≠ 线程数假设服务器8 个物理核心 16 个逻辑 CPU操作系统可能显示CPU 16这是因为开启了 SMT / Hyper-Threading。所以实际开发中经常看到Runtime.getRuntime().availableProcessors()返回16但这不代表你应该创建 16 个 CPU 密集型线程。因为逻辑 CPU ≠ 完全等价的物理 CPU 核心它只是操作系统看到的可用处理器数量。11. 那最大线程数应该怎么确定这里要特别注意maximumPoolSize 不是根据 CPU 核数简单计算出来的。例如8 核机器并不能直接得出maximumPoolSize 16或者maximumPoolSize 32正确思路应该是任务类型 ↓ CPU / IO 比例 ↓ 单任务平均耗时 ↓ 任务并发量 ↓ CPU 利用率 ↓ 内存 ↓ 上下游承载能力 ↓ 压测 ↓ 确定线程池参数12. 为什么线程数太多会有问题每一个 Java 线程都需要栈空间。例如1000 个线程并不是说1000 个线程 1000 个 CPU而是1000 个线程 ↓ 线程栈内存 ↓ 线程调度 ↓ 上下文切换 ↓ CPU 开销最终可能出现线程越来越多 ↓ 上下文切换越来越频繁 ↓ 真正执行任务的效率下降所以线程不是越多越好。13. 为什么最大线程数又不能太小假设8 核 CPU但是你的任务主要是HTTP 数据库 RPC如果设置corePoolSize 2 maximumPoolSize 2可能出现Thread-1 → 等数据库 Thread-2 → 等 HTTP 新的任务 ↓ 只能排队CPU 明明很闲CPU 利用率 ↓ 很低但任务却一直等待。这就是线程数过少无法充分利用机器和 IO 并发能力。14. 核心线程数和最大线程数应该怎么看可以用下面这个思路CPU 密集型 ↓ 线程数量接近 CPU 可用核心数 ↓ 避免大量线程竞争 CPUIO 密集型 ↓ 线程数量可以明显大于 CPU 核数 ↓ 利用线程等待 IO 的时间但最终公式只是起点 ↓ 实际压测 ↓ 观察 CPU 观察 GC 观察线程数 观察队列长度 观察响应时间 观察吞吐量 ↓ 调整参数15. 核心线程数和最大线程数的关系例如corePoolSize8maximumPoolSize32queue1000这并不代表平时 8 压力大马上变成 32实际上8 个核心线程 ↓ 任务进入队列 ↓ 队列不断堆积 ↓ 队列满 ↓ 才开始创建额外线程 ↓ 最多 32所以一个很大的队列可能导致maximumPoolSize 根本很难触发。这也是线程池参数经常配置错误的原因之一。16. 一个比较实用的配置思路假设服务器8 核 应用Java Web 任务HTTP 数据库 Redis不要直接说8 核 → 线程池必须设置 16而应该先从一个合理范围开始例如corePoolSize 816 maximumPoolSize 1632 queue 根据任务量设置然后压测观察CPU 使用率 队列长度 线程数 响应时间 吞吐量 数据库连接数 GC再调整。这里的数字只是压测起点不是通用公式。17. 一个容易忽略的问题数据库连接池假设线程池 maximumPoolSize 100但是数据库连接池 maximumPoolSize 20那么数据库相关任务最多也只能有效使用大约20 个数据库连接剩余线程可能都在等待数据库连接所以线程池不能孤立配置。需要一起考虑线程池 ↓ 数据库连接池 ↓ 数据库以及线程池 ↓ HTTP Client 连接池 ↓ 下游服务18. 最终形成一个完整认识不要记CPU 8 核 → 线程池一定是 8应该记CPU 核数 │ ↓ 任务类型 ↙ ↘ CPU 密集型 IO 密集型 ↓ ↓ 线程接近 CPU 核数 线程可以更多 ↓ ↓ └──────┬─────────┘ ↓ 队列长度 ↓ 最大线程数 ↓ 拒绝策略 ↓ 压测 ↓ 最终参数最重要的三个结论结论 1CPU 核数决定的是 CPU 并行处理能力不是线程池最大线程数。结论 2CPU 密集型任务线程数通常不需要远高于 CPU 核数IO 密集型任务可以使用更多线程因为大量线程会处于 IO 等待。结论 3线程池参数没有一个适用于所有机器和业务的固定公式最终需要结合任务类型、CPU、IO、队列、上下游资源和压测结果确定。
返回列表