线程池
线程池
为什么用线程池
不用线程池的痛点:每次任务来了就 new Thread(),创建 / 销毁线程开销大(涉及系统资源),且线程数不可控,大量并发会撑爆内存。
线程池的好处:复用线程、控制最大并发数、统一管理任务排队,避免频繁创建销毁。
线程池怎么使用
首先最常用的是通过 Executors 工具类快速创建线程池,适合简单场景,不用手动配置复杂参数。
为什么不建议用 Executors 提供的快捷方法
Executors有newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool、newScheduledThreadPool 等,问题在于它们默认的队列:
newFixedThreadPool/newSingleThreadExecutor→ 用的是无界队列LinkedBlockingQueue(默认 Integer.MAX_VALUE)。任务无限堆积,可能内存耗尽 / OOM。newCachedThreadPool→ 核心线程 0,最大线程 Integer.MAX_VALUE,可创建无限线程,可能创建过多线程耗尽资源。newScheduledThreadPool→ 同上,最大线程上限极大。
标准答法:尽量用
new ThreadPoolExecutor手动指定有界队列 + 拒绝策略,避免无界队列和无限线程带来的 OOM 风险。 这也是阿里开发规范明确建议的。
七大核心参数
ThreadPoolExecutor 构造:
1 | new ThreadPoolExecutor( |
提交任务后的执行流程
任务进来,按下面的顺序判断:
- 核心线程没满(当前线程数 < corePoolSize)→ 直接创建核心线程执行。
- 核心线程满了 → 任务放入
workQueue排队。 - 队列也满了 → 创建非核心线程执行(线程数可以到 maximumPoolSize)。
- 线程数已经到 maximumPoolSize,队列也满 → 走拒绝策略。
四种拒绝策略
| 策略 | 行为 | 适用 |
|---|---|---|
| AbortPolicy(默认) | 直接抛 RejectedExecutionException |
任务不能丢,希望快速失败暴露问题 |
| CallerRunsPolicy | 不丢弃,由提交任务的线程自己执行 | 慢速降级、保证任务不丢 |
| DiscardPolicy | 静默丢弃新任务 | 允许丢、可接受丢失 |
| DiscardOldestPolicy | 丢弃队列里最老的任务,再尝试提交新的 | 追求新任务优先 |
合理配置线程数
- CPU 密集型(大量计算,不吃 IO):线程数 ≈ CPU 核数 + 1(或核数),避免过多线程切换反而变慢。
- IO 密集型(等待磁盘 / 网络):线程数 ≈ CPU 核数 × 目标 CPU 利用率 × (1 + 等待时间 / 计算时间)。
线程池异常怎么处理
execute() 提交: 如果任务抛出未捕获的运行时异常,该线程会直接崩溃。异常信息会打印到控制台(或者错误日志),然后线程池会销毁这个旧线程,并创建一个新线程来顶替它。
submit() 提交: 线程池会把任务包装成一个 Future 对象。即使任务内部抛出异常,线程也不会崩溃,异常也不会打印。异常被偷偷“藏”在了 Future 里面,直到你调用 future.get() 时,它才会被重新抛出。
处理方式:
1.execute 提交 + 任务内 try-catch:在 Runnable 里自己捕获。
1 | executor.execute(() -> { |
2.submit 提交 + Future.get ():用 Future 拿结果,get() 会抛出 ExecutionException 包裹原始异常。
1 | Future<?> future = executor.submit(() -> { |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Ling的个人博客!
