Java 线程池深度解析:从零开始理解并发编程的核心工具

这篇文章在讲什么

这篇文章会带你从零开始理解 Java 线程池。

读完后你会明白:

arduino 复制代码
 为什么需要线程池?直接 new Thread 不行吗?
 线程池的 7 个核心参数分别是什么?各自的作用?
 线程池内部的工作流程是怎样的?
 常见的线程池类型有哪些?各自适用什么场景?
 Spring 中怎么使用线程池?
 生产环境中线程池应该怎么配置?
 面试中高频问题怎么回答?

在开始之前,先问你几个问题

scss 复制代码
问题1:如果一个系统每秒有 1000 个请求,每个请求需要并发处理,
      你能每个请求都 new Thread() 吗?会有什么问题?

问题2:线程池的 7 个参数你能说出来吗?它们之间是什么关系?

问题3:corePoolSize 和 maximumPoolSize 都是 10,队列满了之后会怎样?
      如果 corePoolSize 是 5,maximumPoolSize 是 10,队列满了之后又会怎样?

问题4:Spring 中 @Async 注解默认用的是什么线程池?为什么生产环境不推荐用默认的?

问题5:线程池的拒绝策略有几种?各自的行为是什么?

带着这些问题往下读。


第一部分:为什么需要线程池

一、从一个最简单的并发场景开始

假设你有一个 Web 接口,每个请求需要查询三个远程服务:

java 复制代码
// 串行执行:一个一个查,总耗时 = 300 + 200 + 500 = 1000ms
public OrderDetail getOrderDetail(Long orderId) {
    Order order = orderService.query(orderId);           // 300ms
    User user = userService.query(order.getUserId());     // 200ms
    List<Item> items = itemService.query(orderId);        // 500ms
    return new OrderDetail(order, user, items);
}

如果改成并发执行:

java 复制代码
// 并发执行:三个同时查,总耗时 = max(300, 200, 500) = 500ms
public OrderDetail getOrderDetail(Long orderId) {
    Future<Order> orderFuture = executor.submit(() -> orderService.query(orderId));
    Future<User> userFuture = executor.submit(() -> userService.query(order.getUserId()));
    Future<List<Item>> itemsFuture = executor.submit(() -> itemService.query(orderId));
    
    Order order = orderFuture.get();
    User user = userFuture.get();
    List<Item> items = itemsFuture.get();
    return new OrderDetail(order, user, items);
}

耗时从 1000ms 降到 500ms。 这里的 executor 就是线程池。

二、为什么不直接 new Thread?

java 复制代码
// 最原始的方式:每个任务创建一个线程
new Thread(() -> orderService.query(orderId)).start();
new Thread(() -> userService.query(userId)).start();
new Thread(() -> itemService.query(orderId)).start();

看起来能用,但生产环境完全不行。问题有三个:

问题一:创建和销毁线程的开销很大

markdown 复制代码
创建一个线程需要做什么?

1. 在 JVM 中分配内存(每个线程默认 1MB 栈空间)
2. 在操作系统内核中创建线程(系统调用)
3. 上下文切换的准备工作

销毁一个线程需要做什么?

1. 释放 JVM 内存
2. 通知操作系统回收资源

这些操作的耗时大约在 0.1ms ~ 1ms 之间。
听起来不多?算一笔账:
ini 复制代码
假设你的系统每秒处理 1000 个请求
每个请求创建 3 个线程
每秒创建 3000 个线程

创建开销:3000 × 0.5ms = 1500ms
每个线程 1MB 栈空间:3000 × 1MB = 3GB 内存

而且这些线程执行完就销毁了,下次请求又重新创建。

大量时间花在了创建和销毁线程上,而不是执行业务逻辑。

问题二:无法控制并发数量

java 复制代码
// 假设突然来了 10000 个请求
for (int i = 0; i < 10000; i++) {
    new Thread(() -> doSomething()).start();
    // 瞬间创建 10000 个线程
}
arduino 复制代码
10000 个线程 × 1MB 栈空间 = 10GB 内存
操作系统能创建的线程数有上限(通常几千到几万)
超过限制 → OutOfMemoryError: unable to create new native thread
服务器直接崩溃

问题三:没有统一管理

复制代码
线程创建了就不管了
不知道有多少线程在运行
不知道哪些任务在排队
无法优雅地关闭
无法监控线程状态

三、线程池的思路:复用线程

arduino 复制代码
线程池的核心思路:

不是每个任务创建一个线程,而是预先创建一批线程,放在一个"池子"里。
有任务来了,从池子里取一个线程来执行。
执行完后,线程不销毁,放回池子里,等下一个任务。

就像餐厅的服务员:
  不是每个客人来了临时招聘一个服务员,用完就辞退
  而是预先雇佣一批服务员,哪个客人需要就派哪个过去
css 复制代码
没有线程池:
  请求1 → 创建线程A → 执行 → 销毁线程A
  请求2 → 创建线程B → 执行 → 销毁线程B
  请求3 → 创建线程C → 执行 → 销毁线程C

有了线程池:
  启动时 → 创建线程A、B、C 放在池子里
  请求1 → 取线程A → 执行 → 线程A 放回池子
  请求2 → 取线程A → 执行 → 线程A 放回池子
  请求3 → 取线程B → 执行 → 线程B 放回池子

线程被复用了,创建和销毁的开销只发生一次。


第二部分:线程池的 7 个核心参数

一、先看创建线程池的代码

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,                                      // corePoolSize
    10,                                     // maximumPoolSize
    60,                                     // keepAliveTime
    TimeUnit.SECONDS,                       // unit
    new ArrayBlockingQueue<>(100),          // workQueue
    new ThreadFactory() { ... },            // threadFactory
    new ThreadPoolExecutor.AbortPolicy()    // handler
);

这 7 个参数就是线程池的全部配置。 逐个拆解:

二、逐个解释

参数 1:corePoolSize(核心线程数)

ini 复制代码
线程池中始终保持存活的线程数量,即使它们处于空闲状态。

比如 corePoolSize = 5:
  线程池启动后,即使没有任何任务,也会保持 5 个线程存活。
  有任务来了,优先用这 5 个线程来执行。
  任务执行完,这 5 个线程不销毁,继续等待下一个任务。

参数 2:maximumPoolSize(最大线程数)

ini 复制代码
线程池允许创建的最大线程数量。

比如 maximumPoolSize = 10:
  最多同时运行 10 个线程。
  核心线程 5 个都在忙,队列也满了,才会创建额外的线程(最多再创建 5 个)。
  额外的线程空闲超过 keepAliveTime 后会被销毁。

参数 3:keepAliveTime(空闲线程存活时间)

ini 复制代码
非核心线程空闲时的存活时间。

比如 keepAliveTime = 60 秒:
  额外创建的线程(第 6~10 个)如果 60 秒内没有新任务,就会被销毁。
  核心线程不受这个参数影响(默认情况下核心线程不会被销毁)。

参数 4:unit(时间单位)

复制代码
keepAliveTime 的时间单位。
TimeUnit.SECONDS、TimeUnit.MILLISECONDS 等。

参数 5:workQueue(任务队列)

ini 复制代码
核心线程都在忙时,新任务放在这个队列里等待。

比如 workQueue = new ArrayBlockingQueue<>(100):
  最多容纳 100 个等待的任务。
  核心线程执行完当前任务后,从队列里取下一个任务来执行。

参数 6:threadFactory(线程工厂)

arduino 复制代码
用来创建新线程的工厂。
可以自定义线程名称、优先级等。

为什么需要自定义?
  默认创建的线程名字是 "pool-1-thread-1"、"pool-1-thread-2"
  出了问题看日志,根本不知道是哪个线程池的线程
  自定义命名后:"order-pool-thread-1"、"payment-pool-thread-2"
  一目了然。
java 复制代码
// 自定义线程工厂
ThreadFactory threadFactory = new ThreadFactory() {
    private int count = 0;
    
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setName("order-pool-thread-" + (++count));
        return thread;
    }
};

参数 7:handler(拒绝策略)

复制代码
线程池满了(线程数达到 maximumPoolSize,队列也满了)时,
新提交的任务怎么处理?

4 种内置拒绝策略:

AbortPolicy(默认)    → 直接抛出 RejectedExecutionException 异常
CallerRunsPolicy       → 由提交任务的线程自己来执行这个任务
DiscardPolicy          → 直接丢弃任务,不抛异常
DiscardOldestPolicy    → 丢弃队列中最早的任务,把新任务放进队列

三、7 个参数的关系------用一张图理解

markdown 复制代码
你提交一个任务
    ↓
当前运行的线程数 < corePoolSize?
    ├── 是 → 创建一个核心线程来执行任务
    └── 否 → 把任务放进 workQueue
                  ↓
              workQueue 没满?
                  ├── 是 → 任务在队列里排队等待
                  └── 否(队列满了)
                          ↓
                      当前线程数 < maximumPoolSize?
                          ├── 是 → 创建一个非核心线程来执行任务
                          └── 否(线程数也满了)→ 执行拒绝策略

用一段伪代码表示:

java 复制代码
public void execute(Runnable task) {
    if (当前线程数 < corePoolSize) {
        创建新线程执行任务;
    } else if (workQueue.offer(task)) {
        // 队列没满,任务入队等待
    } else if (当前线程数 < maximumPoolSize) {
        创建非核心线程执行任务;
    } else {
        执行拒绝策略;
    }
}

四、一个完整的例子

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,                                      // 核心线程 5 个
    10,                                     // 最多 10 个线程
    60, TimeUnit.SECONDS,                   // 非核心线程空闲 60 秒后销毁
    new ArrayBlockingQueue<>(100),          // 队列容量 100
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
);
复制代码
场景模拟:

提交第 1~5 个任务:
  → 核心线程数 5,当前 0 个,创建 5 个核心线程执行
  → 状态:5 个线程在运行,队列空

提交第 6~105 个任务(假设前 5 个还没执行完):
  → 核心线程都在忙,任务进入队列
  → 状态:5 个线程在运行,队列里 100 个任务在等

提交第 106 个任务:
  → 核心线程都在忙,队列也满了(100 个)
  → 当前线程数 5 < 最大线程数 10
  → 创建第 6 个线程(非核心线程)来执行
  → 状态:6 个线程在运行,队列满

提交第 107~110 个任务:
  → 同上,继续创建非核心线程
  → 状态:10 个线程在运行,队列满

提交第 111 个任务:
  → 核心线程都在忙,队列满了,线程数也到最大了
  → 执行拒绝策略:CallerRunsPolicy
  → 由提交任务的线程(比如主线程)自己来执行这个任务

第三部分:常见的线程池类型

一、Executors 工具类提供的线程池

Java 提供了一个 Executors 工具类,可以快速创建线程池:

FixedThreadPool(固定大小线程池)

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(5);
ini 复制代码
corePoolSize = 5
maximumPoolSize = 5(和核心线程数一样)
keepAliveTime = 0(不会超时,因为都是核心线程)
workQueue = LinkedBlockingQueue(无界队列,容量无限)

特点:线程数固定,不会增加也不会减少。

问题:队列是无界的(LinkedBlockingQueue 没有容量限制)。如果任务提交速度超过处理速度,队列会无限增长,最终导致内存溢出。

CachedThreadPool(缓存线程池)

java 复制代码
ExecutorService executor = Executors.newCachedThreadPool();
ini 复制代码
corePoolSize = 0(没有核心线程)
maximumPoolSize = Integer.MAX_VALUE(无限大)
keepAliveTime = 60 秒
workQueue = SynchronousQueue(不存储任务,直接交给线程执行)

特点:有任务就创建线程,线程空闲 60 秒后销毁。

问题:maximumPoolSize 是无限大。如果瞬间提交大量任务,会创建大量线程,可能导致系统资源耗尽。

SingleThreadExecutor(单线程线程池)

java 复制代码
ExecutorService executor = Executors.newSingleThreadExecutor();
ini 复制代码
corePoolSize = 1
maximumPoolSize = 1
workQueue = LinkedBlockingQueue(无界队列)

特点:只有一个线程,所有任务串行执行。保证任务按提交顺序执行。

ScheduledThreadPool(定时线程池)

java 复制代码
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);

特点:支持定时任务和周期性任务。

java 复制代码
// 延迟 3 秒后执行
executor.schedule(() -> System.out.println("延迟任务"), 3, TimeUnit.SECONDS);

// 每隔 5 秒执行一次
executor.scheduleAtFixedRate(() -> System.out.println("周期任务"), 
    0, 5, TimeUnit.SECONDS);

二、为什么阿里规范禁止使用 Executors 创建线程池

阿里巴巴 Java 开发手册明确规定:

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式创建。

原因:

ini 复制代码
FixedThreadPool:
  workQueue = LinkedBlockingQueue(无界队列)
  → 队列无限增长 → 内存溢出

CachedThreadPool:
  maximumPoolSize = Integer.MAX_VALUE
  → 线程无限创建 → 系统资源耗尽

SingleThreadExecutor:
  workQueue = LinkedBlockingQueue(无界队列)
  → 同样有内存溢出风险

正确做法:用 ThreadPoolExecutor 手动创建,明确指定所有参数。

java 复制代码
// ✅ 正确做法
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, 10, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),    // 有界队列,不会无限增长
    new ThreadPoolExecutor.CallerRunsPolicy()
);

三、对比总结

复制代码
线程池类型          核心线程  最大线程  队列类型          问题
──────────────────────────────────────────────────────────
FixedThreadPool    N        N       LinkedBlockingQueue  无界队列,可能OOM
CachedThreadPool   0        MAX     SynchronousQueue     无限线程,可能耗尽资源
SingleThreadExecutor 1     1       LinkedBlockingQueue  无界队列,可能OOM
ThreadPoolExecutor 自定义   自定义   自定义              无,自己控制

第四部分:线程池的工作流程详解

一、execute() 方法的完整流程

java 复制代码
executor.execute(new Runnable() {
    @Override
    public void run() {
        // 你的任务代码
    }
});

当你调用 execute() 提交一个任务时,线程池内部的处理流程:

java 复制代码
public void execute(Runnable command) {
    
    // 第1步:获取当前工作线程数
    int c = ctl.get();
    int workerCount = workerCountOf(c);
    
    // 第2步:如果当前线程数 < 核心线程数
    if (workerCount < corePoolSize) {
        // 创建一个核心线程来执行这个任务
        if (addWorker(command, true))  // true 表示核心线程
            return;
        // 创建失败(比如线程池已关闭),重新获取状态
        c = ctl.get();
    }
    
    // 第3步:核心线程都在忙,尝试把任务放进队列
    if (isRunning(c) && workQueue.offer(command)) {
        // 入队成功
        // 再次检查线程池状态(防止在入队过程中线程池被关闭)
        int recheck = ctl.get();
        if (!isRunning(recheck) && remove(command))
            // 线程池已关闭,执行拒绝策略
            reject(command);
        else if (workerCountOf(recheck) == 0)
            // 线程池在运行但没有线程,创建一个线程
            addWorker(null, false);
    }
    
    // 第4步:队列满了,尝试创建非核心线程
    else if (!addWorker(command, false))  // false 表示非核心线程
        // 创建失败(线程数已到最大值),执行拒绝策略
        reject(command);
}

二、addWorker() 做了什么

java 复制代码
private boolean addWorker(Runnable firstTask, boolean core) {
    // 创建一个 Worker 对象(Worker 包装了线程和任务)
    Worker worker = new Worker(firstTask);
    Thread thread = worker.thread;
    
    // 启动线程
    thread.start();
    
    // Worker 的 run() 方法会循环从队列里取任务执行
}

三、Worker 的工作循环

java 复制代码
// Worker.run()(简化版)
public void run() {
    runWorker(this);
}

final void runWorker(Worker w) {
    Runnable task = w.firstTask;  // 第一个任务
    
    while (task != null || (task = getTask()) != null) {
        // getTask() 从队列里取任务
        // 如果队列为空,getTask() 会阻塞等待
        
        try {
            task.run();  // 执行任务
        } finally {
            task = null;
        }
    }
    
    // 循环退出:线程池关闭了,或者非核心线程超时了
    // 这个线程结束,被回收
}

关键点:getTask() 会从 workQueue 里取任务。如果队列为空,核心线程会一直阻塞等待,非核心线程等待超过 keepAliveTime 后返回 null,线程退出循环,被销毁。

四、submit() 和 execute() 的区别

java 复制代码
// execute():提交 Runnable,没有返回值
executor.execute(() -> doSomething());

// submit():提交 Callable 或 Runnable,返回 Future
Future<String> future = executor.submit(() -> {
    return doSomethingAndReturn();
});
String result = future.get();  // 阻塞等待结果
scss 复制代码
execute() → 提交任务,不关心结果
submit()  → 提交任务,通过 Future 获取结果或异常

submit() 底层也是调用 execute(),只是包装了一层 FutureTask。

第五部分:线程池的监控和调优

一、线程池提供的监控方法

java 复制代码
ThreadPoolExecutor executor = ...;

executor.getPoolSize()          // 当前线程池中的线程数
executor.getActiveCount()       // 正在执行任务的线程数
executor.getCompletedTaskCount() // 已完成的任务数
executor.getTaskCount()          // 已提交的任务总数(包括已完成和正在执行的)
executor.getQueue().size()       // 队列中等待的任务数
executor.getQueue().remainingCapacity()  // 队列剩余容量
executor.getLargestPoolSize()    // 历史最大线程数

二、一个实际的监控示例

java 复制代码
@Component
public class ThreadPoolMonitor {
    
    private static final Logger log = LoggerFactory.getLogger(ThreadPoolMonitor.class);
    
    @Autowired
    private ThreadPoolExecutor executor;
    
    @Scheduled(fixedRate = 5000)  // 每 5 秒打印一次
    public void monitor() {
        log.info("线程池状态:线程数={}, 活跃线程={}, 队列等待={}, 已完成={}, 历史最大={}",
            executor.getPoolSize(),
            executor.getActiveCount(),
            executor.getQueue().size(),
            executor.getCompletedTaskCount(),
            executor.getLargestPoolSize()
        );
    }
}

输出:

复制代码
线程池状态:线程数=8, 活跃线程=5, 队列等待=23, 已完成=1547, 历史最大=10

三、线程池参数怎么配置

没有万能公式,但有一些经验法则:

CPU 密集型任务

diff 复制代码
任务特点:计算量大,一直在使用 CPU(比如加密、压缩、图像处理)
线程数 = CPU 核心数 + 1

为什么?CPU 密集型任务每个线程都在用 CPU,
线程数超过 CPU 核心数只会增加上下文切换的开销,不会提高效率。
+1 是为了当某个线程因为偶尔的页缺失或其他原因暂停时,多出来的线程可以利用 CPU。
java 复制代码
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    cpuCores + 1, cpuCores + 1, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(200)
);

IO 密集型任务

scss 复制代码
任务特点:大量时间在等待 IO(比如网络请求、数据库查询、文件读写)
线程数 = CPU 核心数 × 2  或  CPU 核心数 / (1 - 阻塞系数)

为什么?IO 密集型任务在等待 IO 时 CPU 是空闲的,
可以多开线程让 CPU 在等待期间处理其他任务。

阻塞系数 = 等待时间 / 总时间
如果一个任务 80% 时间在等 IO,20% 在用 CPU:
  线程数 = CPU 核心数 / (1 - 0.8) = CPU 核心数 × 5
java 复制代码
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    cpuCores * 2, cpuCores * 2, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(500)
);

实际生产环境的经验值

复制代码
场景                    建议核心线程数    建议最大线程数    队列大小
───────────────────────────────────────────────────────────────
HTTP 请求处理           CPU核心数 × 2    CPU核心数 × 4    200~500
数据库批量操作          5~10             10~20            100~200
消息消费                消费者数量        消费者数量 × 2   50~100
异步通知                3~5              5~10             100~200

核心原则:先用小参数上线,通过监控数据逐步调优。不要一上来就设很大的值。


第六部分:Spring 中使用线程池

一、Spring 中配置线程池

java 复制代码
@Configuration
public class ThreadPoolConfig {
    
    @Bean("orderExecutor")
    public ThreadPoolExecutor orderExecutor() {
        return new ThreadPoolExecutor(
            5,
            10,
            60, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(200),
            new ThreadFactory() {
                private final AtomicInteger count = new AtomicInteger(0);
                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r);
                    thread.setName("order-pool-" + count.incrementAndGet());
                    return thread;
                }
            },
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
}

二、使用 @Async 异步执行

java 复制代码
@Service
public class OrderService {
    
    @Async("orderExecutor")  // 指定使用哪个线程池
    public void sendNotification(Order order) {
        // 这个方法会在 orderExecutor 线程池中异步执行
        notificationClient.send(order);
    }
}
java 复制代码
// 调用方
public void placeOrder(Order order) {
    orderRepository.save(order);
    orderService.sendNotification(order);  // 异步执行,不阻塞
    return "下单成功";  // 立即返回,不用等通知发送完
}

注意:@Async 基于 AOP 代理,和 @Transactional 一样有同类调用不生效的问题。

三、@Async 默认线程池的问题

java 复制代码
// 如果不指定线程池,@Async 使用 Spring 默认的 SimpleAsyncTaskExecutor
@Async  // 没有指定线程池
public void sendNotification(Order order) { ... }
复制代码
SimpleAsyncTaskExecutor 的问题:
  每次调用都创建一个新线程,不复用
  等于没有线程池
  高并发下会创建大量线程,系统崩溃

生产环境必须自定义线程池并指定。

四、CompletableFuture + 线程池(现代并发编程)

java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private ThreadPoolExecutor orderExecutor;
    
    public OrderDetail getOrderDetail(Long orderId) {
        
        // 三个查询并发执行
        CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(
            () -> orderRepository.findById(orderId), orderExecutor);
        
        CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
            () -> userService.findByOrderId(orderId), orderExecutor);
        
        CompletableFuture<List<Item>> itemsFuture = CompletableFuture.supplyAsync(
            () -> itemService.findByOrderId(orderId), orderExecutor);
        
        // 等待所有结果
        CompletableFuture.allOf(orderFuture, userFuture, itemsFuture).join();
        
        // 组装结果
        return new OrderDetail(
            orderFuture.join(),
            userFuture.join(),
            itemsFuture.join()
        );
    }
}
java 复制代码
// 更强大的链式操作
CompletableFuture.supplyAsync(() -> orderRepository.findById(orderId), orderExecutor)
    .thenApply(order -> {
        // 拿到订单后,查用户
        User user = userService.findByUserId(order.getUserId());
        return new OrderDetail(order, user);
    })
    .thenApply(detail -> {
        // 拿到订单+用户后,查商品
        List<Item> items = itemService.findByOrderId(detail.getOrder().getId());
        detail.setItems(items);
        return detail;
    })
    .exceptionally(ex -> {
        // 任何一步出错,走这里
        log.error("查询订单详情失败", ex);
        return null;
    });

第七部分:常见问题和坑

一、线程池中的异常处理

java 复制代码
executor.execute(() -> {
    int result = 1 / 0;  // 抛出 ArithmeticException
});
scss 复制代码
execute() 提交的任务:
  异常会直接抛出,打印到控制台
  如果不捕获,这个线程会终止
  线程池会创建一个新线程来替代它

submit() 提交的任务:
  异常不会直接抛出,被包装在 Future 里
  调用 future.get() 时才会抛出 ExecutionException
  如果不调用 future.get(),异常会被静默吞掉
java 复制代码
// submit() 的正确用法
Future<String> future = executor.submit(() -> {
    return doSomething();
});

try {
    String result = future.get();
} catch (ExecutionException e) {
    // 通过 getCause() 拿到真正的异常
    Throwable cause = e.getCause();
    log.error("任务执行失败", cause);
}

二、线程池的优雅关闭

java 复制代码
// ❌ 错误:直接关闭
executor.shutdownNow();  // 立即中断所有线程,正在执行的任务也会被中断

// ✅ 正确:优雅关闭
executor.shutdown();  // 不再接受新任务,等待已提交的任务执行完
boolean terminated = executor.awaitTermination(30, TimeUnit.SECONDS);
if (!terminated) {
    // 30 秒内还有任务没执行完,强制关闭
    executor.shutdownNow();
}
scss 复制代码
shutdown()  vs  shutdownNow()

shutdown():
  不再接受新任务
  已提交的任务会继续执行完
  正在执行的任务不会被中断

shutdownNow():
  不再接受新任务
  尝试中断正在执行的任务
  返回队列中还没执行的任务列表

三、ThreadLocal 和线程池的坑

java 复制代码
// ThreadLocal 存储用户信息
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();

// 在请求处理中设置
currentUser.set(user);

// 在业务代码中获取
User user = currentUser.get();

问题:线程池中的线程是复用的。

css 复制代码
请求1 → 线程A 执行 → currentUser.set(user1)
请求1 结束 → 线程A 放回池子 → currentUser 里还有 user1

请求2 → 线程A 执行 → currentUser.get() 拿到的是 user1 ❌
                        应该是 user2,但上一个请求没清理

解决:用完必须清理。

java 复制代码
try {
    currentUser.set(user);
    // 业务逻辑
} finally {
    currentUser.remove();  // 必须清理
}

更好的方案:用拦截器统一处理。

java 复制代码
@Component
public class UserInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                             HttpServletResponse response, 
                             Object handler) {
        User user = extractUser(request);
        UserContext.set(user);  // 设置
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler, Exception ex) {
        UserContext.remove();  // 清理
    }
}

第八部分:回顾与检验

回顾

markdown 复制代码
你从这篇文章学到了什么:

1. 为什么需要线程池
   - 创建和销毁线程有开销,线程池复用线程
   - 控制并发数量,防止系统资源耗尽
   - 统一管理和监控

2. 7 个核心参数
   - corePoolSize:核心线程数
   - maximumPoolSize:最大线程数
   - keepAliveTime:非核心线程空闲存活时间
   - unit:时间单位
   - workQueue:任务队列
   - threadFactory:线程工厂
   - handler:拒绝策略

3. 工作流程
   - 核心线程未满 → 创建核心线程
   - 核心线程满了 → 任务入队
   - 队列满了 → 创建非核心线程
   - 线程数到最大 → 执行拒绝策略

4. 4 种拒绝策略
   - AbortPolicy:抛异常
   - CallerRunsPolicy:调用者自己执行
   - DiscardPolicy:静默丢弃
   - DiscardOldestPolicy:丢弃最老的任务

5. Spring 中使用
   - 自定义 ThreadPoolExecutor Bean
   - @Async 指定线程池
   - CompletableFuture 链式并发

现在回答开头的问题

问题1:每秒 1000 个请求,每个都 new Thread() 会怎样?

会创建大量线程,每个线程占 1MB 栈空间,很快耗尽内存。而且创建和销毁线程的开销远大于执行任务本身的开销。系统会崩溃。

问题2:7 个参数是什么?

corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。

问题3:corePoolSize 和 maximumPoolSize 都是 10,队列满了会怎样?

直接执行拒绝策略,因为没有空间再创建非核心线程了(已经到最大值了)。如果 core=5、max=10、队列满了,会创建非核心线程(第 6~10 个)来处理。

问题4:@Async 默认用什么线程池?

SimpleAsyncTaskExecutor,每次调用都创建新线程,不复用。生产环境必须自定义线程池。

问题5:拒绝策略有几种?

四种:AbortPolicy(抛异常)、CallerRunsPolicy(调用者执行)、DiscardPolicy(丢弃)、DiscardOldestPolicy(丢弃最老的)。

相关推荐
每天进步一点_JL2 小时前
Spring 到底在做什么?从零开始理解 Java 企业开发的核心框架
后端·spring
每天进步一点_JL2 小时前
Spring 【多实现切换 & 事务代理机制】深度解析
后端
彩票管理中心秘书长2 小时前
MySQL 数据库高级与网络管理操作命令大全
后端
Gopher_HBo2 小时前
CompletableFuture函数原理
后端
香山上的麻雀10082 小时前
由 Rust 开发的能大幅降低LLM token消耗的高性能 CLI 代理工具 rtk
开发语言·后端·rust
神奇小汤圆3 小时前
Java vs Go:哈希冲突解决之道,为什么一个用红黑树,一个用桶?
后端
神奇小汤圆3 小时前
得物二面:Redis 中某个 Key 访问量特别大怎么办?我:Redis 能顶得住... 生瓜蛋子 生瓜蛋子
后端
掘金者阿豪3 小时前
Spring Data JPA 接入金仓数据库:少写代码,多干活
前端·后端
Moment3 小时前
AI 时代,为什么全栈项目越来越离不开 Monorepo 和 TypeScript
前端·javascript·后端