少女祈祷中...
NEKO
知识库

java 线程池面试要点

一、基础概念

Q1:什么是线程池?为什么需要线程池?

答: 线程池是线程的复用容器,预先创建或按需创建线程,统一管理生命周期。

使用线程池的好处:

好处

说明

降低资源消耗

复用线程,减少创建/销毁开销

提高响应速度

任务到达时无需等待创建线程

提高可管理性

统一分配、调优、监控

防止资源耗尽

控制并发数,避免线程爆炸

// 不用线程池:每次新建,开销大
new Thread(() -> doWork()).start();

// 用线程池:复用线程
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.execute(() -> doWork());

Q2:ThreadPoolExecutor 的七大参数是什么?

答:

public ThreadPoolExecutor(
    int corePoolSize,                  // 1. 核心线程数
    int maximumPoolSize,               // 2. 最大线程数
    long keepAliveTime,                // 3. 空闲存活时间
    TimeUnit unit,                     // 4. 时间单位
    BlockingQueue<Runnable> workQueue, // 5. 任务队列
    ThreadFactory threadFactory,       // 6. 线程工厂
    RejectedExecutionHandler handler   // 7. 拒绝策略
)

参数

含义

corePoolSize

核心线程数,即使空闲也保留(除非 allowCoreThreadTimeOut)

maximumPoolSize

最大线程数 = 核心 + 非核心

keepAliveTime

非核心线程空闲存活时间

unit

keepAliveTime 的时间单位

workQueue

任务队列,存放待执行任务

threadFactory

创建线程的工厂,可自定义线程名

handler

拒绝策略,队列满且线程达上限时触发


Q3:corePoolSize 和 maximumPoolSize 有什么区别?

答:

  • corePoolSize:核心线程数,线程池长期保持的线程数

  • maximumPoolSize:最大线程数,临时扩容的上限

关系:

corePoolSize ≤ 当前线程数 ≤ maximumPoolSize

关键点:

  • 核心线程默认不回收(除非 allowCoreThreadTimeOut(true))

  • 非核心线程空闲超过 keepAliveTime 会被回收

  • 只有当队列满时,才会创建非核心线程


Q4:keepAliveTime 的作用是什么?

答: 控制非核心线程的空闲存活时间:

// 非核心线程空闲 60 秒后回收
new ThreadPoolExecutor(2, 10, 60L, TimeUnit.SECONDS, ...);

注意:

  • 默认只作用于非核心线程

  • 调用 allowCoreThreadTimeOut(true) 后,核心线程也会被回收

  • 回收的是空闲线程,忙碌线程不受影响


Q5:workQueue 有哪些类型?如何选择?

答: 常见队列:

队列

特点

适用

ArrayBlockingQueue

有界,数组实现

推荐,可控

LinkedBlockingQueue

可选有界,链表

Executors 默认用无界版

SynchronousQueue

不存储,直接交接

高吞吐,CachedThreadPool

PriorityBlockingQueue

优先级排序

有优先级需求

DelayQueue

延迟执行

定时任务

// 有界队列,推荐
new ArrayBlockingQueue<>(100);

// 无界队列,有 OOM 风险
new LinkedBlockingQueue<>();

选择原则:必须用有界队列,避免任务堆积导致 OOM。


Q6:threadFactory 的作用是什么?

答: 自定义线程创建方式,常用于:

  1. 设置有意义的线程名(排查问题)

  2. 设置守护线程

  3. 设置优先级、异常处理器

ThreadFactory factory = new ThreadFactory() {
    private final AtomicInteger count = new AtomicInteger(1);
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, "my-pool-" + count.getAndIncrement());
        t.setDaemon(false);
        t.setUncaughtExceptionHandler((thread, e) -> 
            log.error("线程异常", e));
        return t;
    }
};

推荐:用 Guava 的 ThreadFactoryBuilder 更简洁。


二、工作流程

Q7:线程池的完整工作流程是什么?

答: 提交任务时,按以下顺序处理:

提交任务
   ↓
1. 核心线程未满? → 创建核心线程执行
   ↓ 否
2. 队列未满? → 放入队列等待
   ↓ 否
3. 线程数 < max? → 创建非核心线程执行
   ↓ 否
4. 执行拒绝策略

流程图:

execute(task)
    │
    ├─ 线程数 < corePoolSize → addWorker(core)
    │
    ├─ 队列未满 → workQueue.offer(task)
    │
    ├─ 线程数 < maximumPoolSize → addWorker(non-core)
    │
    └─ 拒绝策略

关键:先队列,后扩容,这是最容易记错的点。


Q8:为什么线程池先入队列,而不是先创建线程?

答: 这是设计权衡:

  • 创建线程开销大,能复用就复用

  • 队列能缓冲任务,避免频繁创建销毁

  • 只有队列满(说明任务积压)才扩容

但这也带来问题:

  • 如果队列无界,maximumPoolSize 永远用不到

  • 任务会一直堆积,可能导致 OOM

最佳实践:用有界队列,让扩容和拒绝策略生效。


Q9:核心线程是何时创建的?

答: 默认懒创建——提交任务时才创建。

预创建核心线程:

ThreadPoolExecutor pool = new ThreadPoolExecutor(...);
pool.prestartAllCoreThreads();  // 启动所有核心线程
pool.prestartCoreThread();      // 启动一个核心线程

好处:提前创建,避免首次任务延迟。


Q10:线程池中的线程是如何复用的?

答: 通过 runWorker 循环 + getTask 阻塞:

final void runWorker(Worker w) {
    Runnable task = w.firstTask;
    w.firstTask = null;
    while (task != null || (task = getTask()) != null) {
        task.run();  // 执行任务
        task = null;
    }
}

流程:

  1. Worker 线程启动后进入 runWorker

  2. 循环从队列 getTask() 取任务

  3. 队列空则阻塞(take() 或 poll(keepAliveTime))

  4. 取到任务执行,执行完继续循环

  5. 线程不销毁,实现复用


Q11:线程池中的线程何时销毁?

答:

  • 非核心线程:空闲超过 keepAliveTime 后,getTask() 返回 null,退出循环销毁

  • 核心线程:默认不销毁,除非 allowCoreThreadTimeOut(true)

  • 线程池关闭:shutdown() / shutdownNow() 后销毁

// getTask 中的回收逻辑
if ((wc > maximumPoolSize || (timed && timedOut))
    && (wc > 1 || workQueue.isEmpty())) {
    if (compareAndDecrementWorkerCount(c))
        return null;  // 返回 null,线程退出
}

Q12:execute() 和 submit() 有什么区别?

答:

对比项

execute()

submit()

接口

Executor

ExecutorService

参数

Runnable

Runnable / Callable

返回值

void

Future

异常

抛到线程

封装在 Future 中

// execute:异常直接抛出
pool.execute(() -> { throw new RuntimeException(); });

// submit:异常在 get() 时抛出
Future<?> f = pool.submit(() -> { throw new RuntimeException(); });
f.get(); // 抛 ExecutionException

注意:submit() 的异常不 get() 就被吞掉,容易踩坑。


Q13:线程池的 ctl 字段是什么?

答: ctl 是 AtomicInteger,打包了线程数和状态:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// 高 3 位:线程池状态
// 低 29 位:工作线程数
private static final int COUNT_BITS = Integer.SIZE - 3; // 29
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// 状态
RUNNING    = -1 << 29  // 接受新任务,处理队列任务
SHUTDOWN   =  0 << 29  // 不接受新任务,处理队列任务
STOP       =  1 << 29  // 不接受新任务,不处理队列,中断线程
TIDYING    =  2 << 29  // 所有任务终止,即将调用 terminated()
TERMINATED =  3 << 29  // terminated() 完成

优点:一个原子变量同时管理状态和数量,避免锁。


Q14:线程池的五种状态如何流转?

答:

RUNNING ──shutdown()──→ SHUTDOWN ──队列空+线程空──→ TIDYING ──→ TERMINATED
   │                                                    
   └──shutdownNow()──→ STOP ──线程空──→ TIDYING ──→ TERMINATED

状态

说明

RUNNING

正常运行,接受新任务

SHUTDOWN

不接受新任务,处理完队列任务

STOP

不接受新任务,不处理队列,中断线程

TIDYING

所有任务终止,准备清理

TERMINATED

完全终止


Q15:shutdown() 和 shutdownNow() 有什么区别?

答:

对比项

shutdown()

shutdownNow()

新任务

拒绝

拒绝

队列任务

继续执行

丢弃,返回 List

运行中任务

不中断

尝试中断

状态

SHUTDOWN

STOP

返回值

void

List<Runnable>

pool.shutdown();     // 优雅关闭
pool.shutdownNow();  // 强制关闭,返回未执行任务

最佳实践:

pool.shutdown();
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
    pool.shutdownNow();
}

Q16:isShutdown() 和 isTerminated() 有什么区别?

答:

  • isShutdown():是否已调用 shutdown() 或 shutdownNow(),状态 ≥ SHUTDOWN

  • isTerminated():是否完全终止,状态 == TERMINATED

pool.shutdown();
System.out.println(pool.isShutdown());   // true
System.out.println(pool.isTerminated()); // false,可能还有任务
// 等待所有任务完成
pool.awaitTermination(1, TimeUnit.MINUTES);
System.out.println(pool.isTerminated()); // true

三、拒绝策略

Q17:什么是拒绝策略?何时触发?

答: 当线程数达到 maximumPoolSize 且队列已满时,新任务被拒绝。

触发条件:

  1. 线程数 == maximumPoolSize

  2. 队列已满

  3. 提交新任务

// 触发拒绝的场景
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    1, 1, 0, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1)); // 队列容量 1

pool.execute(task1); // 核心线程执行
pool.execute(task2); // 入队列
pool.execute(task3); // 触发拒绝策略

Q18:JDK 内置的四种拒绝策略是什么?

答:

策略

行为

风险

AbortPolicy

抛 RejectedExecutionException

默认,需捕获

CallerRunsPolicy

调用者线程执行

阻塞提交线程

DiscardPolicy

静默丢弃

任务丢失无感知

DiscardOldestPolicy

丢弃队列最老任务,重试提交

老任务丢失

// 1. AbortPolicy(默认)
new ThreadPoolExecutor.AbortPolicy();

// 2. CallerRunsPolicy
new ThreadPoolExecutor.CallerRunsPolicy();

// 3. DiscardPolicy
new ThreadPoolExecutor.DiscardPolicy();

// 4. DiscardOldestPolicy
new ThreadPoolExecutor.DiscardOldestPolicy();

Q19:CallerRunsPolicy 的原理和适用场景?

答: 拒绝时,由提交任务的线程自己执行:

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        r.run();  // 调用者线程执行
    }
}

效果:

  • 提交线程被阻塞,无法继续提交

  • 形成负反馈,自然降速

  • 保证任务不丢失

适用:

  • 不允许丢任务

  • 能接受提交线程变慢

  • 如日志、非核心业务

风险:若提交线程是关键线程(如 IO 线程),可能拖垮整体。


Q20:如何自定义拒绝策略?

答: 实现 RejectedExecutionHandler:

public class CustomRejectPolicy implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        // 1. 记录日志
        log.warn("任务被拒绝: {}", r);
        
        // 2. 持久化到数据库/MQ,后续补偿
        saveToDb(r);
        
        // 3. 或降级处理
        // fallback(r);
    }
}

常见自定义策略:

  • 持久化后重试

  • 降级到本地执行

  • 报警 + 丢弃

  • 阻塞等待(类似 CallerRuns)


Q21:为什么推荐 CallerRunsPolicy 或自定义策略?

答:

  • AbortPolicy:抛异常,若未捕获则任务丢失

  • DiscardPolicy:静默丢弃,问题难发现

  • DiscardOldestPolicy:丢老任务,可能丢关键任务

  • CallerRunsPolicy:不丢任务,但阻塞提交者

推荐:

  • 核心业务:自定义(持久化 + 重试)

  • 非核心:CallerRunsPolicy

  • 绝不推荐:DiscardPolicy(无感知丢失)


Q22:拒绝策略能保证任务不丢失吗?

答: 取决于策略:

策略

是否丢任务

AbortPolicy

丢(抛异常)

CallerRunsPolicy

不丢

DiscardPolicy

丢

DiscardOldestPolicy

丢老任务

自定义持久化

不丢(可补偿)

结论:只有 CallerRuns 和自定义持久化能保证不丢。


四、线程池大小设置

Q23:如何设置线程池大小?

答: 根据任务类型决定:

任务类型

公式

说明

CPU 密集型

N + 1

N = CPU 核数

IO 密集型

2N 或 2N+1

等待 IO 时可切换

混合型

拆分或动态调整

分别处理

int cpuCores = Runtime.getRuntime().availableProcessors();

// CPU 密集型
int cpuPoolSize = cpuCores + 1;

// IO 密集型
int ioPoolSize = cpuCores * 2;

原因:

  • CPU 密集型:线程多了上下文切换开销大

  • IO 密集型:线程等待 IO 时 CPU 空闲,可多开


Q24:为什么 CPU 密集型是 N+1?

答:

  • N 个线程跑满 CPU

  • +1 是为了容错:某线程偶尔缺页/暂停时,第 N+1 个能顶上

  • 太多线程会导致上下文切换开销

验证:CPU 利用率接近 100%,但线程数不宜过多。


Q25:为什么 IO 密集型是 2N?

答:

  • 线程等待 IO 时不占 CPU

  • 可以多开线程,让 CPU 在等待时切换执行其他任务

  • 2N 是经验值,实际要看IO 等待时间 / CPU 计算时间的比值

更精确的公式:

线程数 = N × (1 + 等待时间 / 计算时间)

// 例:CPU 4 核,等待 100ms,计算 50ms
// 线程数 = 4 × (1 + 100/50) = 12

Q26:线程池大小设置过大或过小有什么问题?

答:

问题

过小

过大

吞吐量

低,任务排队

上下文切换多

响应时间

长

可能更短

CPU

利用率低

切换开销大

内存

占用少

线程栈占内存

稳定性

任务积压

资源耗尽

结论:不是越大越好,要压测调优。


Q27:如何动态调整线程池大小?

答: ThreadPoolExecutor 提供 setter:

ThreadPoolExecutor pool = new ThreadPoolExecutor(...);
pool.setCorePoolSize(20);
pool.setMaximumPoolSize(50);
pool.setKeepAliveTime(30, TimeUnit.SECONDS);

注意:

  • 调大 core:立即生效,可能创建新线程

  • 调小 core:空闲线程会被回收

  • 调小 max:若当前线程数 > 新 max,多余线程会被回收

结合配置中心可实现动态调整(如 Nacos + 定时任务)。


Q28:如何监控线程池状态?

答: 通过以下方法:

pool.getPoolSize();          // 当前线程数
pool.getCorePoolSize();      // 核心线程数
pool.getMaximumPoolSize();   // 最大线程数
pool.getActiveCount();       // 活跃线程数
pool.getQueue().size();      // 队列任务数
pool.getCompletedTaskCount();// 已完成任务数
pool.getTaskCount();         // 总任务数
pool.getLargestPoolSize();   // 历史最大线程数

监控指标:

  • 活跃度 = activeCount / maximumPoolSize

  • 队列使用率 = queue.size() / queue.capacity()

  • 拒绝次数(自定义策略统计)


Q29:为什么阿里规范禁止用 Executors 创建线程池?

答: 因为 Executors 的默认实现有 OOM 风险:

方法

问题

newFixedThreadPool

无界队列,任务堆积 OOM

newSingleThreadExecutor

无界队列,同上

newCachedThreadPool

最大线程 Integer.MAX_VALUE,线程爆炸

newScheduledThreadPool

无界队列,同上

// 危险:无界队列
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>()); // 无界!
}

推荐:手动 new ThreadPoolExecutor,指定有界队列和拒绝策略。


Q30:如何合理配置一个线程池?

答: 完整示例:

ThreadPoolExecutor pool = new ThreadPoolExecutor(
    8,                              // core:CPU 核数
    16,                             // max:2 倍
    60L, TimeUnit.SECONDS,          // 空闲回收
    new ArrayBlockingQueue<>(200),  // 有界队列
    new ThreadFactoryBuilder()
        .setNameFormat("biz-pool-%d")
        .build(),
    new ThreadPoolExecutor.CallerRunsPolicy() // 不丢任务
);

// 允许核心线程超时回收
pool.allowCoreThreadTimeOut(true);

// 预启动核心线程
pool.prestartAllCoreThreads();

配置原则:

  1. 有界队列

  2. 有意义的线程名

  3. 明确的拒绝策略

  4. 根据任务类型定大小

  5. 监控 + 动态调整


五、源码与进阶

Q31:addWorker 的作用是什么?

答: 创建并启动工作线程:

private boolean addWorker(Runnable firstTask, boolean core) {
    // 1. CAS 增加 workerCount
    // 2. new Worker(firstTask)
    // 3. 启动线程 t.start()
    // 4. 加入 workers 集合
}

要点:

  • core=true:检查是否 < corePoolSize

  • core=false:检查是否 < maximumPoolSize

  • 失败返回 false,触发拒绝策略


Q32:getTask() 如何实现阻塞和超时?

答:

private Runnable getTask() {
    boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
    for (;;) {
        if (timed) {
            // 非核心线程:超时获取
            Runnable r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
            if (r != null) return r;
            timedOut = true;
        } else {
            // 核心线程:阻塞获取
            Runnable r = workQueue.take();
            if (r != null) return r;
        }
        // 超时或状态异常,返回 null,线程退出
    }
}

区别:

  • 核心线程:take() 永久阻塞

  • 非核心线程:poll(keepAliveTime) 超时返回 null


Q33:Worker 为什么要继承 AQS?

答: Worker 继承 AQS 是为了控制中断状态:

private final class Worker extends AbstractQueuedSynchronizer 
        implements Runnable {
    final Thread thread;
    Runnable firstTask;
    volatile long completedTasks;
}

原因:

  • 线程池在 shutdown 时要区分:任务执行中 vs 空闲

  • 用 AQS 的 state 表示是否持有锁

  • 持有锁:正在执行任务,不应中断

  • 未持有锁:空闲,可中断

避免:中断正在执行的任务导致数据不一致。


Q34:线程池如何处理异常?

答:

提交方式

异常处理

execute()

抛到线程,未捕获则线程死亡

submit()

封装在 Future,get() 时抛 ExecutionException

execute 的异常:

pool.execute(() -> {
    throw new RuntimeException("error");
});
// 线程打印异常后死亡,线程池补充新线程

submit 的异常:

Future<?> f = pool.submit(() -> {
    throw new RuntimeException("error");
});
try {
    f.get(); // 抛 ExecutionException
} catch (ExecutionException e) {
    Throwable cause = e.getCause(); // 原始异常
}

最佳实践:任务内 try-catch,避免线程死亡。


Q35:线程池中的线程死了会怎样?

答: 线程池会补充新线程:

  • 线程因异常退出,processWorkerExit 被调用

  • workerCount 减 1

  • 如果线程池还在运行,会 addWorker(null, false) 补充

// processWorkerExit 简化
if (runState < STOP) {
    addWorker(null, false); // 补充线程
}

注意:频繁异常退出会导致线程反复创建,性能下降。


Q36:如何优雅关闭线程池?

答:

public void shutdownGracefully(ExecutorService pool) {
    pool.shutdown(); // 1. 拒绝新任务,处理队列
    try {
        if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
            pool.shutdownNow(); // 2. 超时强制关闭
            if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
                System.err.println("线程池未终止");
            }
        }
    } catch (InterruptedException e) {
        pool.shutdownNow(); // 3. 当前线程中断,也强制关闭
        Thread.currentThread().interrupt(); // 恢复中断
    }
}

要点:

  1. 先 shutdown() 优雅关闭

  2. 超时后 shutdownNow() 强制关闭

  3. 处理 InterruptedException

  4. 恢复中断标志


Q37:线程池的队列满了但线程没满,会发生什么?

答: 这是常见误解,实际流程是:

  1. 核心线程满

  2. 先入队列

  3. 队列满

  4. 才创建非核心线程

  5. 线程满 + 队列满

  6. 拒绝策略

关键:队列优先于扩容。

例外:SynchronousQueue 不存储任务,直接创建线程。


Q38:SynchronousQueue 的线程池行为有何不同?

答: SynchronousQueue 容量为 0,不存储任务:

new ThreadPoolExecutor(0, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<>()); // CachedThreadPool

流程:

  1. 提交任务

  2. 尝试交给空闲线程

  3. 无空闲线程 → 创建新线程

  4. 线程数达 max → 拒绝

特点:

  • 任务不排队,直接交接

  • 适合短任务、高吞吐

  • 风险:线程数可能爆炸


Q39:线程池如何实现任务窃取?

答: ForkJoinPool 用工作窃取:

  • 每个线程有自己的双端队列

  • 线程从自己队列头部取任务

  • 空闲时从其他线程队列尾部偷任务

线程A: [任务1, 任务2, 任务3]  ← 从头部取
线程B: [任务4, 任务5]         ← 空闲,从A尾部偷任务3

优点:减少竞争,提高 CPU 利用率。

适用:分治任务(如并行流、归并排序)。


Q40:ThreadPoolExecutor 和 ForkJoinPool 有什么区别?

答:

对比项

ThreadPoolExecutor

ForkJoinPool

队列

共享队列

每线程独立队列

调度

FIFO

工作窃取

任务

独立任务

可分治任务

适用

通用

递归/并行

代表

Executors

parallelStream


六、实战与踩坑

Q41:线程池参数如何根据业务调整?

答: 分场景:

场景1:Web 请求处理(IO 密集)

core = 20, max = 50, queue = 500
// 请求多,IO 等待长

场景2:CPU 计算(CPU 密集)

core = N+1, max = N+1, queue = 100
// 计算为主,线程不宜多

场景3:批量任务

core = 10, max = 10, queue = 1000
// 稳定吞吐,队列缓冲

原则:压测 + 监控 + 动态调整。


Q42:线程池任务执行超时怎么办?

答: 三种方案:

  1. Future.get(timeout):

Future<?> f = pool.submit(task);
try {
    f.get(3, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    f.cancel(true); // 中断任务
}
  1. 任务内自检:

pool.execute(() -> {
    long start = System.currentTimeMillis();
    while (!Thread.interrupted()) {
        if (System.currentTimeMillis() - start > 3000) break;
        // 工作...
    }
});
  1. 独立超时线程:定时扫描 + 取消


Q43:线程池导致 OOM 的原因有哪些?

答:

  1. 无界队列:任务堆积,队列 OOM

  2. 线程数过多:线程栈占内存

  3. 任务持有大对象:无法释放

  4. ThreadLocal 未清理:内存泄漏

// 危险:无界队列
new LinkedBlockingQueue<>();

// 安全:有界队列
new ArrayBlockingQueue<>(1000);

排查:jmap + jstack + Arthas。


Q44:如何排查线程池问题?

答:

  1. jstack:查看线程栈

jstack <pid> | grep "pool-"
  1. Arthas:

thread --state WAITING
thread -n 3  # 最忙的3个线程
  1. 自定义监控:

scheduledExecutor.scheduleAtFixedRate(() -> {
    log.info("pool: active={}, queue={}, completed={}",
        pool.getActiveCount(),
        pool.getQueue().size(),
        pool.getCompletedTaskCount());
}, 0, 10, TimeUnit.SECONDS);
  1. 拒绝策略统计:自定义 handler 计数


Q45:线程池的线程名为什么重要?

答:

  • 排查问题:jstack 中一眼识别

  • 监控告警:按业务分组

  • 日志追踪:MDC 传递上下文

// 不好的命名
Thread-0, Thread-1, pool-1-thread-1

// 好的命名
order-pool-1, payment-pool-2, sms-pool-1

推荐:业务名-pool-序号。

评论