ThreadPoolExecutor、任务队列与拒绝策略 | JavaSE

ThreadPoolExecutor、任务队列与拒绝策略

一、学习目标

完成本章后,你应该能够:

  • 独立写出 ThreadPoolExecutor 的核心七参数构造方式。
  • 准确解释 corePoolSizemaximumPoolSizekeepAliveTimeunitworkQueuethreadFactoryhandler
  • 理解“核心线程数”和“最大线程数”之间的关系。
  • 理解教材所谓“临时线程”在 ThreadPoolExecutor 中真正指什么。
  • 完整说出一个新任务提交到线程池后的处理顺序。
  • 准确判断什么时候进入任务队列、什么时候创建额外工作线程、什么时候发生拒绝。
  • 理解有界队列、无界队列和直接移交队列的基本差异。
  • 掌握 ArrayBlockingQueue 在线程池中的基本用途。
  • 掌握 JDK 提供的四种标准拒绝策略。
  • 准确理解 CallerRunsPolicy 中的“Caller”是谁,而不是机械理解成 main 线程。
  • 能够根据一组具体线程池参数推导任务 1、2、3……分别去哪。
  • 理解为什么线程池配置本质上是在平衡吞吐量、延迟和资源上限。
  • 能够识别常见线程池参数配置风险。

二、核心知识

2.1 ThreadPoolExecutor 是什么

ThreadPoolExecutor 是 Java 中经典的:

可配置线程池实现类。

上一章:

ExecutorService pool =
        Executors.newFixedThreadPool(3);

是使用一个预配置方案。

这一章我们直接:

ExecutorService pool =
        new ThreadPoolExecutor(...);

自己决定:

核心线程数
最大线程数
空闲回收时间
任务队列
线程工厂
拒绝策略

这意味着我们终于能够回答:

当线程全忙了以后,到底发生什么?


2.2 七参数构造器

原课程主线使用:

public ThreadPoolExecutor(
        int corePoolSize,
        int maximumPoolSize,
        long keepAliveTime,
        TimeUnit unit,
        BlockingQueue<Runnable> workQueue,
        ThreadFactory threadFactory,
        RejectedExecutionHandler handler
)

例如:

ExecutorService pool =
        new ThreadPoolExecutor(
                3,
                5,
                10,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

可以把它理解为:

正式员工:3人
最大员工:5人
最多临时增加:2人

临时人员工空闲10秒后可被回收

等待区容量:3

HR:ThreadFactory

客满以后的处理规则:
CallerRunsPolicy

这个“餐厅/公司”模型非常适合入门。


三、七个参数详解

3.1 corePoolSize:核心线程数量

corePoolSize = 3

表示:

线程池的核心规模 = 3

当任务到来,并且当前运行的工作线程数量小于:

3

ThreadPoolExecutor 会优先创建新的工作线程处理任务。

例如:

corePoolSize = 3

任务1:

创建线程1

任务2:

创建线程2

任务3:

创建线程3

所以:

前3个任务

会逐步建立核心规模的工作线程。


3.2 核心线程是不是线程池创建时就一定已经存在?

不一定。

默认情况下,核心工作线程也是:

新任务到来时
↓
按需创建

而不是:

new ThreadPoolExecutor()
↓
立即自动创建全部corePoolSize线程

如果确实希望提前启动核心线程,可以使用:

prestartCoreThread();

或:

prestartAllCoreThreads();

但这属于补充能力。

当前最重要的是:

corePoolSize = 3 表示核心线程规模,不表示构造器执行瞬间一定已经存在3条线程。


3.3 maximumPoolSize:最大线程数量

例如:

maximumPoolSize = 5

表示线程池中的工作线程数:

最多允许达到5

而:

corePoolSize = 3
maximumPoolSize = 5

意味着:

核心规模:3

最多额外扩展:
5 - 3 = 2

教材经常把超过核心数量的部分叫:

临时线程

例如:

线程1
线程2
线程3
→ 核心规模

线程4
线程5
→ 压力高时额外创建

3.4 “临时线程”是教学概念

需要准确理解:

ThreadPoolExecutor API 并没有定义一个:

TemporaryThread

类型。

所谓:

临时线程

指的是:

当前线程池规模超过 corePoolSize 后,为处理压力而额外存在的工作线程。

所以正式术语仍然是:

worker thread
工作线程

只不过在解释:

corePoolSize
maximumPoolSize

时,把超过核心规模的线程称为:

临时线程

更容易入门。


3.5 keepAliveTime

例如:

keepAliveTime = 10
unit = TimeUnit.SECONDS

共同表示:

超过核心规模的工作线程在空闲达到指定时间后,可以被回收。

例如:

core = 3
max = 5

高峰期:

5条线程

任务减少以后:

线程4空闲
线程5空闲

持续:

10秒

以后,它们可能被终止。

最终线程池重新收缩到:

核心规模附近

3.6 keepAliveTime 默认主要影响谁

默认情况下,keep-alive 策略主要应用于:

超过 corePoolSize 的工作线程

核心线程通常不会仅仅因为空闲超过该时间就自动退出。

ThreadPoolExecutor 也提供:

allowCoreThreadTimeOut(true);

让核心线程也允许超时回收。

但这是额外配置。

本章基本模型仍然是:

核心线程
→ 长期保留

超过核心规模的线程
→ 空闲过久可以回收

3.7 unit:时间单位

keepAliveTime 只是数字:

10

必须配合:

TimeUnit unit

才有完整含义。

例如:

10, TimeUnit.SECONDS

代表:

10秒

而:

10, TimeUnit.MINUTES

代表:

10分钟

常见单位:

TimeUnit.SECONDS
TimeUnit.MILLISECONDS
TimeUnit.MINUTES
TimeUnit.HOURS

3.8 workQueue:任务队列

例如:

new ArrayBlockingQueue<>(3)

表示:

任务等待队列容量 = 3

当核心规模已经达到:

corePoolSize

并且这些工作线程没有立即处理新任务时,新任务通常优先:

进入任务队列

而不是立刻创建超过核心规模的新线程。

这是 ThreadPoolExecutor 最重要的执行规则之一。


3.9 threadFactory:线程工厂

例如:

Executors.defaultThreadFactory()

线程池自己不会凭空拥有 Thread。

当需要新工作线程时,会通过:

ThreadFactory

创建线程。

可以把它理解成:

线程工厂
=
负责“招聘线程”的HR

默认线程工厂会创建普通平台线程。

实际工程中还可以自定义 ThreadFactory:

设置线程名称
设置异常处理
调整线程属性

例如线程名称:

order-pool-1
order-pool-2

比:

pool-3-thread-7

更方便日志诊断。


3.10 handler:拒绝策略

类型:

RejectedExecutionHandler

作用:

当线程池无法接受一个新任务时,决定如何处理这个任务。

例如:

工作线程全部达到最大数量
+
任务队列也满

此时再来一个任务:

怎么办?

答案就在:

拒绝策略

四、任务进入 ThreadPoolExecutor 的完整流程

这是整章最核心的内容。

假设:

corePoolSize = 3
maximumPoolSize = 5
queue capacity = 3

可以先记住一句话:

先核心线程,再队列,再扩到最大线程,最后拒绝。

完整流程:

提交新任务
   │
   ▼
当前线程数 < corePoolSize?
   │
   ├── 是
   │    ↓
   │  创建新工作线程
   │  直接处理任务
   │
   └── 否
        ↓
     尝试进入任务队列
        │
        ├── 队列没满
        │    ↓
        │   任务排队
        │
        └── 队列满
             ↓
       当前线程数 < maximumPoolSize?
             │
             ├── 是
             │    ↓
             │  创建额外工作线程
             │  直接处理任务
             │
             └── 否
                  ↓
                拒绝任务

简化:

1. core
2. queue
3. max
4. reject

这是必须闭卷记住的顺序。


五、用具体任务推演一次

线程池:

new ThreadPoolExecutor(
        3,
        5,
        10,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(3),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy()
);

假设所有任务执行时间很长,提交过程中前面的任务都还没有结束。


5.1 任务1

当前:

线程数 = 0
core = 3

因为:

0 < 3

所以:

创建线程1
↓
执行任务1

5.2 任务2

当前线程数 = 1
1 < 3

所以:

创建线程2
↓
执行任务2

5.3 任务3

当前线程数 = 2
2 < 3

所以:

创建线程3
↓
执行任务3

此时:

核心规模已达到3

5.4 任务4

核心线程都在忙。

线程池不会马上创建:

线程4

而是先:

尝试入队

队列:

[任务4]

5.5 任务5

[任务4, 任务5]

5.6 任务6

[任务4, 任务5, 任务6]

队列达到容量:

3

5.7 任务7

现在:

核心线程全忙
+
队列已满

但是:

当前线程数 = 3
maximumPoolSize = 5

所以可以扩容。

创建:

线程4

直接执行:

任务7

5.8 任务8

同理:

当前线程数 = 4
4 < 5

创建:

线程5

执行:

任务8

5.9 任务9

此时:

线程数 = 5
达到maximumPoolSize

队列 = 3
也已满

所有容量都耗尽。

因此:

任务9
↓
进入拒绝策略

所以本例容量模型是:

直接执行:
任务1、2、3

队列:
任务4、5、6

额外线程直接执行:
任务7、8

任务9:
拒绝

当然这是假设:

前面的任务在提交过程中一直没有完成。

真实程序中如果某些任务已经执行结束,结果就可能变化。


六、什么时候才创建超过核心规模的线程

这是高频易错题。

错误理解:

核心线程都忙了,马上创建临时线程。

不准确。

正确顺序:

核心线程都忙
↓
先尝试把任务加入队列

只有:

队列也无法继续接收

并且:

当前线程数量仍小于maximumPoolSize

才会:

创建超过核心规模的工作线程

也就是教材所称:

临时线程

所以:

核心忙 + 队列满 + 尚未达到最大线程数 → 才扩容。


七、什么时候拒绝任务

典型饱和拒绝条件:

线程数量达到 maximumPoolSize
+
任务队列无法再接收任务

又来了新任务。

那么:

RejectedExecutionHandler

开始工作。

除此之外还有一个非常重要的情况:

线程池已经 shutdown 后,再提交新任务,同样会被拒绝。

所以不能只背:

线程满 + 队列满

还要知道:

Executor已经关闭

也不能接受任务。


八、任务队列

8.1 BlockingQueue

ThreadPoolExecutor 的:

BlockingQueue<Runnable> workQueue

保存的是:

等待线程池执行的任务

不是:

Thread对象

这一点非常重要:

线程池里管理线程
任务队列里放Runnable任务

8.2 ArrayBlockingQueue

原课程示例:

new ArrayBlockingQueue<>(3)

它是:

有界阻塞队列

容量明确:

3

优点:

任务排队数量有清晰上限

因此:

线程数量有限
+
队列容量有限

系统的任务承载边界更加明确。

当全部达到上限:

触发拒绝策略

8.3 有界队列为什么重要

如果队列无限增长:

任务不断进入
↓
处理速度跟不上
↓
等待任务越来越多

虽然线程数没有爆炸:

队列本身也可能消耗大量内存

所以:

限制线程数

只是资源治理的一部分。

还需要考虑:

限制队列容量

8.4 无界队列

例如:

new LinkedBlockingQueue<>()

如果没有人为指定较小容量,可形成非常大的等待空间。

ThreadPoolExecutor 使用这种队列时:

核心线程达到corePoolSize
↓
后续任务持续进入队列

因为队列通常一直可以接收:

就不会因为“队列满”触发扩展到maximumPoolSize

也就是说:

maximumPoolSize

在这种典型配置中可能基本发挥不了扩容作用。

风险:

任务提交速度长期大于消费速度
↓
队列不断增长
↓
内存压力

8.5 SynchronousQueue

还有一种非常特殊的队列:

SynchronousQueue

它并不是普通意义上:

存很多等待任务

而更像:

直接把任务交给一个可用工作线程

如果没有线程可以立即接手:

入队就无法像普通队列那样成功

于是 ThreadPoolExecutor 可能:

尝试创建新线程

这是一种:

直接移交(Direct Handoff)

策略。

本阶段了解即可。


九、四种标准拒绝策略

9.1 AbortPolicy

new ThreadPoolExecutor.AbortPolicy()

这是默认拒绝策略。

行为:

任务无法接受
↓
抛出 RejectedExecutionException

优点:

问题不会悄悄消失
调用方能够知道任务提交失败

例如:

try {

    pool.execute(task);

} catch (RejectedExecutionException e) {

    System.out.println(
            "任务被拒绝"
    );
}

适合:

不允许任务静默丢失

的场景。


9.2 DiscardPolicy

new ThreadPoolExecutor.DiscardPolicy()

行为:

任务无法接受
↓
直接丢弃
↓
不主动抛异常

最大问题:

任务可能悄悄消失

例如:

订单任务
支付任务
数据保存任务

显然不能随便使用这种策略。

它只适合:

任务本身确实允许丢弃

的少数场景。


9.3 DiscardOldestPolicy

new ThreadPoolExecutor.DiscardOldestPolicy()

基本行为:

线程池饱和
↓
丢弃队列头部等待最久的任务
↓
重新尝试提交当前新任务

例如:

队列:
A B C

新任务D到来

可能:

丢弃A
↓
重新尝试提交D

注意:

它并不是简单地“删一个旧任务,然后一定成功”。

重新提交仍然可能再次遇到饱和情况。

而且:

默默丢掉一个已经等待很久的任务

通常风险很大。


9.4 CallerRunsPolicy

new ThreadPoolExecutor.CallerRunsPolicy()

这是非常有意思的策略。

当线程池无法执行新任务时:

由调用 execute() 的那个线程直接执行任务的 run()

假设:

main线程
↓
pool.execute(task)

线程池饱和。

那么:

Caller = main

此时 main 可能执行任务。

但是如果:

HTTP请求线程
↓
pool.execute(task)

那么 Caller 就可能是:

HTTP请求线程

而不是:

main

因此必须记:

CallerRunsPolicy
=
调用 execute 的线程执行

不能机械记成:

主线程执行

9.5 CallerRunsPolicy 为什么有“反馈控制”效果

假设提交任务的线程:

一直疯狂向线程池提交任务

线程池满了以后:

CallerRunsPolicy
↓
提交者自己被迫执行任务

于是提交线程暂时不能:

继续高速提交

形成:

线程池越忙
↓
提交者越可能自己处理
↓
提交速度自动下降

这是一种简单的:

反压 / 反馈控制思想

9.6 四种策略对比

| 策略 | 行为 | | --------------------- | -------------------------------------- | | AbortPolicy | 拒绝并抛 RejectedExecutionException | | DiscardPolicy | 静默丢弃当前任务 | | DiscardOldestPolicy | 丢弃队列中最旧等待任务,再重试当前任务 | | CallerRunsPolicy | 由调用 execute() 的线程执行任务 |

最不能死记硬背的是:

CallerRunsPolicy

因为它不是:

永远main执行

而是:

谁提交
谁可能执行

十、自定义拒绝策略

因为:

RejectedExecutionHandler

本身是接口。

所以也可以自定义:

RejectedExecutionHandler handler =
        (task, executor) -> {

            System.out.println(
                    "任务被拒绝:" + task
            );

        };

然后:

new ThreadPoolExecutor(
        ...,
        handler
);

实际工程可能需要:

记录日志
报警
写入备用队列
降级
返回错误

而不是简单使用四种标准策略。

本阶段知道:

拒绝策略可扩展

即可。


十一、完整线程池示例

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {

    public static void main(String[] args) {

        ExecutorService pool =
                new ThreadPoolExecutor(
                        3,
                        5,
                        10,
                        TimeUnit.SECONDS,
                        new ArrayBlockingQueue<>(3),
                        Executors.defaultThreadFactory(),
                        new ThreadPoolExecutor.CallerRunsPolicy()
                );

        for (int i = 1; i <= 10; i++) {

            int taskId = i;

            pool.execute(() -> {

                String name =
                        Thread.currentThread()
                              .getName();

                System.out.println(
                        name
                        + "正在执行任务"
                        + taskId
                );

                try {

                    Thread.sleep(2000);

                } catch (InterruptedException e) {

                    Thread.currentThread()
                          .interrupt();
                }
            });
        }

        pool.shutdown();
    }
}

这个例子可以用于观察:

核心线程
任务排队
最大线程扩容
CallerRunsPolicy

但实际哪一个具体任务最终走到哪条线程,受到:

任务执行速度
线程调度
提交时机

影响。

不要根据某一次运行输出推断绝对固定任务编号。


十二、原理与进阶

12.1 为什么线程池不是“线程越多越好”

假设:

CPU只有8个核心

却创建:

10000个平台线程

如果它们都是 CPU 密集任务:

大家都在抢CPU

可能导致:

大量上下文切换
调度成本
缓存局部性下降

线程数量增加不再带来性能提升。


12.2 CPU 密集型任务

CPU 密集型(CPU-bound)任务主要耗费:

计算能力

例如:

大量数学计算
编码
复杂算法
图像处理

这种任务线程数一般不能无限增加。

因为:

真正的稀缺资源是CPU核心

12.3 I/O 密集型任务

I/O 密集型(I/O-bound)任务存在大量:

网络等待
文件等待
数据库等待

线程可能长期:

不占用CPU计算

因此经典平台线程模型下,有时可以允许比 CPU 核心数更多的工作线程。

但是究竟设置多少:

没有一个适用于所有系统的固定公式

要结合:

任务特征
响应时间
吞吐量
CPU
内存
外部依赖
实际压测

确定。

所以不要背:

CPU核数 × 2

然后把它当成永恒真理。


12.4 队列越大也不是越好

大队列:

优点:
不容易马上拒绝任务

但:

缺点:
任务可能排很久
内存占用增加
请求延迟越来越高

所以:

“没有拒绝”

并不意味着:

“系统健康”

有时一个请求:

排队5分钟以后才执行

虽然没有被拒绝,业务上仍然已经失败。


12.5 maximumPoolSize 越大也不是越好

最大线程数越大:

能暂时吞下更多并发任务

但同时也意味着可能消耗:

更多内存
更多系统线程
更多调度成本
更多下游连接

甚至:

把数据库先打死

例如:

线程池1000线程
↓
1000个任务同时请求数据库
↓
数据库连接池只有50

最终并不会凭空提升系统性能。


12.6 线程池配置实际上是容量设计

所以 ThreadPoolExecutor 七参数背后的真正问题不是:

“面试的时候怎么把七参数背出来?”

而是:

系统最多允许多少任务同时执行?
↓
多少任务允许等待?
↓
最多等待多久?
↓
超过容量怎么办?

因此线程池是:

资源治理与容量控制工具。


十三、Executors 与 ThreadPoolExecutor 的配置认识

上一章学习:

Executors.newFixedThreadPool(n)

JDK 21 官方说明它使用:

固定数量工作线程
+
共享无界队列

这意味着:

线程数有界

但:

等待任务数量没有较小的明确上限

如果生产速度长期大于消费速度:

任务可以持续堆积

所以在需要:

明确容量边界

的系统中,自行配置:

ThreadPoolExecutor

和:

ArrayBlockingQueue

能够让:

最大线程数
+
最大排队任务数
+
拒绝策略

更加显式。

但这不是说:

Executors API 错了

而是:

预配置线程池是否合适,要根据任务负载模型决定。


十四、常见问题

14.1 corePoolSize=3,构造线程池时就一定有3条线程吗?

默认不一定。

通常在任务到来时按需创建。


14.2 核心线程都忙了以后是不是马上创建临时线程?

不是。

先:

尝试入队

队列也无法接收以后,才考虑扩到:

maximumPoolSize

14.3 maximumPoolSize=5,是不是永远有5条线程?

不是。

它表示:

最大允许数量

不是固定数量。


14.4 keepAliveTime 会不会默认把核心线程也销毁?

默认主要针对:

超过 corePoolSize 的空闲线程

核心线程可以通过额外配置允许超时。


14.5 workQueue 里面放的是 Thread 吗?

不是。

放的是:

等待执行的 Runnable 任务

14.6 队列没满时,会创建超过核心规模的线程吗?

按照 ThreadPoolExecutor 的典型执行规则:

核心规模达到以后
↓
优先入队

因此只有队列无法接收任务以后,才尝试扩展线程数量。


14.7 队列满了就一定拒绝吗?

不一定。

如果:

当前线程数 < maximumPoolSize

还可以:

创建额外工作线程

14.8 什么情况下才典型地进入拒绝策略?

线程已达到maximumPoolSize
+
队列无法再接收

或者:

线程池已经关闭

14.9 CallerRunsPolicy 是 main 线程执行吗?

不一定。

准确说:

调用 execute() 的线程执行。

main 提交:

main执行

其他业务线程提交:

那个业务线程执行

14.10 DiscardOldestPolicy 是不是推荐策略?

不能因为名字看起来合理就随意使用。

它会:

丢弃已经在队列中等待的任务

这可能导致严重业务问题。


14.11 队列越大是不是越安全?

不是。

大队列会降低立即拒绝概率,但会增加:

内存压力
排队时间
请求延迟

14.12 线程越多是不是吞吐量越高?

不是。

超过系统实际承载能力以后,更多线程可能只会增加竞争和调度成本。


十五、练习与验收

15.1 知识问答

  1. ThreadPoolExecutor 是什么?
  2. 七参数分别是什么?
  3. corePoolSize 表示什么?
  4. maximumPoolSize 表示什么?
  5. 教材中的“临时线程”指什么?
  6. keepAliveTimeunit 为什么必须结合理解?
  7. workQueue 保存的是什么?
  8. ThreadFactory 负责什么?
  9. RejectedExecutionHandler 负责什么?
  10. 核心线程都忙以后,新任务第一步通常做什么?
  11. 什么时候才创建超过核心规模的线程?
  12. 什么时候拒绝任务?
  13. 为什么有界队列有利于资源治理?
  14. 无界队列有什么潜在风险?
  15. SynchronousQueue 与普通存储型队列有什么基本区别?
  16. 四种标准拒绝策略分别是什么?
  17. 默认拒绝策略是什么?
  18. CallerRunsPolicy 中 Caller 是谁?
  19. 为什么 CallerRunsPolicy 具有一定反压效果?
  20. 为什么线程池参数不存在适用于所有系统的万能数字?

15.2 代码阅读

线程池:

ExecutorService pool =
        new ThreadPoolExecutor(
                2,
                4,
                10,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );

假设所有任务都执行很久,并连续快速提交:

任务1
任务2
任务3
任务4
任务5
任务6
任务7

回答:

  1. 任务1、2会怎样处理?
  2. 任务3、4可能去哪里?
  3. 任务5、6为什么可能触发额外工作线程?
  4. 任务7为什么可能被拒绝?
  5. 整个线程池最多同时有几条工作线程?
  6. 队列最多保存几个等待任务?

再分析:

new ThreadPoolExecutor(
        3,
        100,
        10,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(),
        factory,
        handler
);

回答:

  1. 当核心规模达到3以后,任务通常先去哪?
  2. 如果队列基本没有容量上限,什么时候才容易触发创建第4条线程?
  3. 为什么 maximumPoolSize=100 可能长期发挥不了预想中的作用?
  4. 这种配置最大的资源风险转移到了哪里?

15.3 手写代码

任务一:七参数线程池

从零手写:

new ThreadPoolExecutor(...)

要求:

核心线程:3
最大线程:5
空闲回收:10秒
队列容量:3
默认线程工厂
CallerRunsPolicy

然后提交:

10个耗时任务

每个任务输出:

线程名称
任务编号

任务二:观察拒绝

创建:

core = 1
max = 2
queue = 1

所有任务:

sleep较长时间

快速提交多个任务。

分别替换:

AbortPolicy
DiscardPolicy
CallerRunsPolicy

观察现象。


任务三:CallerRunsPolicy

要求输出:

Thread.currentThread().getName()

证明:

被 CallerRunsPolicy 接管的任务可能由提交任务的线程执行。

15.4 Debug

下面配置:

new ThreadPoolExecutor(
        5,
        3,
        10,
        TimeUnit.SECONDS,
        queue,
        factory,
        handler
);

判断:

corePoolSize
maximumPoolSize

之间有什么逻辑问题。


有人说:

core=3、max=5,所以第4个任务来了以后线程池一定立刻创建第4条线程。

指出错误。


有人说:

我用了无界队列,所以永远不会拒绝任务,因此系统绝对最稳定。

分析这句话的问题。


有人说:

CallerRunsPolicy 就是把任务交给 main 线程。

指出不准确之处。

15.5 综合训练

设计一个“秋招岗位信息解析服务”线程池。

系统不断收到:

JD解析任务

假设:

单个任务需要一定CPU计算
并且偶尔访问文件

请你设计:

corePoolSize
maximumPoolSize
queue capacity
keepAliveTime
rejection policy

第一版参数。

不要求得到所谓“唯一正确答案”。

真正要求:

  1. 解释为什么设置这个核心线程数。
  2. 为什么设置这个最大线程数。
  3. 为什么队列不能无限大。
  4. 队列满以后为什么选择该拒绝策略。
  5. 如何通过压测进一步调整参数。
  6. 如果任务平均处理速度低于任务到达速度很久,会发生什么?

15.6 本章验收

必须能够闭卷画出:

新任务
  ↓
线程数 < core?
  ├─ 是 → 创建线程执行
  └─ 否
       ↓
     尝试入队
       ↓
     队列成功?
       ├─ 是 → 等待
       └─ 否
            ↓
       线程数 < max?
            ├─ 是 → 创建额外线程执行
            └─ 否 → 拒绝

并能够闭卷说出七参数:

corePoolSize
maximumPoolSize
keepAliveTime
unit
workQueue
threadFactory
handler

再闭卷说出四种拒绝策略:

AbortPolicy
DiscardPolicy
DiscardOldestPolicy
CallerRunsPolicy

最后能够解释:

为什么线程池不是线程越多越好
为什么队列不是越大越好
为什么ThreadPoolExecutor本质上是在管理系统容量

做到这些,就说明 JavaSE 多线程核心主线已经形成完整闭环:

线程是什么
↓
如何创建线程
↓
如何描述任务
↓
如何获得异步结果
↓
线程如何运行
↓
为什么线程不安全
↓
如何同步与加锁
↓
如何统一管理线程
↓
如何限制线程池容量