【JUC | 学习笔记】—— 线程池

读完这篇,你会彻底搞懂线程池的每一个参数是干什么的、为什么需要阻塞队列、拒绝策略什么时候触发,以及在传统 Spring 和 Spring Boot 中分别怎么配。

一、线程池能干什么

一句话:复用线程,减少创建销毁的开销。

打个比方。你开了一家银行网点:

复制代码
没有线程池(每次 new Thread):
  来一个客户 → 招一个柜员 → 办完 → 开除
  再来一个   → 再招一个柜员 → 办完 → 开除
  ↑ 招人和开除的成本比办业务本身还高

有线程池:
  先招 5 个柜员常驻窗口(核心线程)
  来客户了 → 喊个柜员去办 → 办完回窗口等着(复用)
  客户太多 → 从备班名单里再拉几个柜员(扩容到最大线程数)
  大厅站不下了 → 按规矩来(拒绝策略)

线程池解决三个问题

问题 没线程池 有线程池
资源消耗 每个任务 new 一个线程,用完销毁 线程复用,不重复创建
响应速度 new Thread 需要时间 线程就绪,来了直接用
管理能力 线程数不可控,内存可能爆 统一管理,有上限

二、核心概念

先看一张全景图:

2.1 任务(Task)------"客户的业务"

复制代码
// Runnable:干完活没返回值
Runnable task = () -> 办存款();

// Callable:干完活有返回值
Callable<String> task = () -> { 办存款(); return "已入账"; };

2.2 核心线程数(corePoolSize)------"常驻窗口柜员"

不管有没有客户,这 N 个柜员一直在窗口守着。 (除非设置了 allowCoreThreadTimeOut

复制代码
// 核心线程 = 5,相当于大厅常设 5 个窗口,柜员准时上班
// 没客户的时候他们就在窗口等着,不会走

2.3 阻塞队列(BlockingQueue)------"大厅等候区"

窗口全满,新来的客户先取号排队。

复制代码
5 个窗口全在办业务
        ↓
  第 6 个客户来了 → 取号,在等候区坐着等 ← 阻塞队列
  第 7 个客户来了 → 取号,在等候区坐着等
  ...
  等候区坐满了! ← 触发扩容

三种常用队列:

队列类型 特点 适用场景
LinkedBlockingQueue 有界/无界,FIFO 排队 通用
ArrayBlockingQueue 有界,FIFO 必须限制排队人数时
SynchronousQueue 没有等候区,来了必须立刻办 要求立即处理的场景

关键:等候区坐满了才会开新窗口。 所以你的等候区设 5000 个座位,意味着前 5005 个客户都是 5 个窗口在办(5个在办 + 5000个在等)。

2.4 最大线程数(maxPoolSize)------"所有窗口全开 + 备班全上"

复制代码
5 个窗口全忙 + 等候区 5000 个座位全满
        ↓
  第 5006 个客户:没座位了!→ 从备班名单拉人,开新窗口
        ↓
  一直开到 maxPoolSize = 50 个窗口
        ↓
  50 个窗口全在办 + 5000 个在等 = 5050 个客户同时处理中
        ↓
  第 5051 个客户:没窗口、没座位 → 拒绝策略

2.5 拒绝策略(RejectedExecutionHandler)------"大厅站都站不下了怎么办"

复制代码
四种策略:

AbortPolicy(默认)
  "暂停营业!" → 直接抛 RejectedExecutionException
  适用:必须保证任务不能丢的场景

CallerRunsPolicy
  "大堂经理亲自办!" → 提交任务的线程自己执行
  适用:可以接受延迟,但不能丢任务

DiscardPolicy
  "不接了!" → 直接拒绝,不打任何招呼
  适用:可有可无的非关键任务

DiscardOldestPolicy
  "最早取号的那个不办了!" → 请最早排队的走,给新来的腾位置
  适用:宁愿丢旧数据,也要处理最新的

2.6 存活时间(keepAliveTime)------"备班柜员多久没活就下班"

复制代码
常驻柜员:一直上班,不走(除非 allowCoreThreadTimeOut = true)
备班柜员:空闲超过 keepAliveTime 秒,下班走人

窗口数变化曲线:
50 ┤     ╭─╮
   │    ╱   ╲
20 ┤── ╱      ╲──── keepAliveTime 到期,备班柜员下班 ────
5  ┤───────────────────────────────────────────(常驻窗口不变)
   └────────────────────────────────────────────→ 时间
       忙        闲         忙           闲

三、执行流程------任务提交后到底走了哪条路

复制代码
        客户来了 ------ submit(task)
             │
             ▼
     ┌─ 有窗口空着? ── YES ──→ 柜员直接办
     │       │ NO
     │       ▼
     │  等候区有座位? ── YES ──→ 取号排队等
     │       │ NO
     │       ▼
     │  当前窗口数 < maxPoolSize? ── YES ──→ 开新窗口,备班柜员上
     │       │ NO
     │       ▼
     └──→ 执行拒绝策略

关键认知:扩容的发生条件是"核心线程全忙 + 队列全满",而不是"核心线程全忙"。这意味着:

复制代码
核心线程=5,最大线程=50,等候区=5000 个座位

客户数 1-5:    直接开窗口办
客户数 6-5005:  取号坐等候区等(只开了 5 个窗口!)
客户数 5006+:   才开始开 6-50 号窗口

所以如果你设了很大的队列(比如 Integer.MAX_VALUE),最大线程数永远不会用到。

四、原生 Java 实现(不用 Spring)

java 复制代码
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ManualThreadPoolDemo {

    public static void main(String[] args) throws Exception {
        // ========== 1. 自定义线程工厂 ==========
        ThreadFactory threadFactory = new ThreadFactory() {
            private final AtomicInteger count = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("bank-counter-" + count.getAndIncrement());  // 线程名
                t.setDaemon(false);                               // 非守护线程
                t.setUncaughtExceptionHandler((thread, ex) -> {
                    System.err.println(thread.getName() + " 异常: " + ex.getMessage());
                });
                return t;
            }
        };

        // ========== 2. 创建线程池 ==========
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5,                              // 核心线程数
                10,                             // 最大线程数
                30L,                            // 空闲线程存活时间
                TimeUnit.SECONDS,               // 时间单位
                new LinkedBlockingQueue<>(100), // 阻塞队列,容量 100
                threadFactory,                  // 线程工厂
                new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
        );

        // ========== 3. 提交任务 ==========

        // 方式一:Runnable,没返回值
        executor.execute(() -> {
            System.out.println(Thread.currentThread().getName() + " 办了一笔存款");
        });

        // 方式二:Callable + Future,有返回值
        Future<String> future = executor.submit(() -> {
            Thread.sleep(1000);
            return "存款已入账";
        });

        System.out.println("等待办理结果...");
        String result = future.get();  // 阻塞等待
        System.out.println(result);

        // ========== 4. 关闭线程池 ==========
        executor.shutdown();  // 不再接新任务,已提交的跑完
        // executor.shutdownNow();  // 立刻中断所有任务

        boolean terminated = executor.awaitTermination(10, TimeUnit.SECONDS);
        if (terminated) {
            System.out.println("线程池已正常关闭");
        }
    }
}

五、Spring Boot 实现

注意,springboot里子自带了一个配置好的线程池,大部分场景可以直接使用,不用写config配置类,但拒绝策略是固定的。所以下面演示自定义线程池写法。

5.1 配置文件(application.yml)

java 复制代码
thread:
  pool:
    executor:
      config:
        core-pool-size: 10       # 核心线程数
        max-pool-size: 50        # 最大线程数
        keep-alive-time: 30      # 空闲存活时间(秒)
        block-queue-size: 200    # 队列容量
        policy: CallerRunsPolicy # 拒绝策略

5.2 配置类(读取 YML 配置)

java 复制代码
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "thread.pool.executor.config")
public class ThreadPoolConfigProperties {
    private Integer corePoolSize = 10;
    private Integer maxPoolSize = 50;
    private Long keepAliveTime = 30L;
    private Integer blockQueueSize = 200;
    private String policy = "CallerRunsPolicy";
}

5.3 线程池 Bean 注册

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
@Configuration
@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
public class ThreadPoolConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties props) {

        // 拒绝策略
        RejectedExecutionHandler handler;
        switch (props.getPolicy()) {
            case "DiscardPolicy":
                handler = new ThreadPoolExecutor.DiscardPolicy();
                break;
            case "DiscardOldestPolicy":
                handler = new ThreadPoolExecutor.DiscardOldestPolicy();
                break;
            case "CallerRunsPolicy":
                handler = new ThreadPoolExecutor.CallerRunsPolicy();
                break;
            default:
                handler = new ThreadPoolExecutor.AbortPolicy();
        }

        // 线程工厂
        ThreadFactory factory = new ThreadFactory() {
            private final AtomicInteger count = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("biz-pool-" + count.getAndIncrement());
                t.setUncaughtExceptionHandler((thread, ex) ->
                    log.error("线程池异常 {}", thread.getName(), ex)
                );
                return t;
            }
        };

        return new ThreadPoolExecutor(
                props.getCorePoolSize(),
                props.getMaxPoolSize(),
                props.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(props.getBlockQueueSize()),
                factory,
                handler
        );
    }
}

5.4 使用线程池

java 复制代码
@Service
public class MyService {

    @Resource
    private ThreadPoolExecutor threadPoolExecutor;

    public void doAsyncWork() throws Exception {
        // 无返回值
        threadPoolExecutor.execute(() -> {
            System.out.println("异步执行");
        });

        // 有返回值 + CompletableFuture 链式处理
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 业务逻辑
            return "结果";
        }, threadPoolExecutor);

        future.thenAccept(result -> {
            System.out.println("拿到结果:" + result);
        });
    }
}
相关推荐
智者知已应修善业14 小时前
【proteus仿真CD4511抢答器4路】2024-5-13
驱动开发·经验分享·笔记·硬件架构·proteus·硬件工程
kinl201814 小时前
cs236_note1 (lec5-lec6) VAEs
笔记
还在忙碌的吴小二14 小时前
Spring Boot Examples 学习示例集新手入门指南
java·spring boot·后端·学习·spring
吃好睡好便好14 小时前
说说如何爱护头发
学习·生活
.千余14 小时前
【测试】测试用例设计攻略(6大设计方法)
服务器·网络·笔记·学习·测试用例
searchforAI14 小时前
Obsidian一键获取视频笔记内容,AI做知识管理+内容创作
人工智能·笔记·gpt·学习·知识图谱·markdown·知识库
暴躁小师兄数据学院14 小时前
【AI大模型应用开发工程师特训笔记】第04讲(第6章):复合数据类型
人工智能·windows·笔记·python
kevinoop14 小时前
机器人视觉学习记录
学习·机器人
民乐团扒谱机14 小时前
【太奶学IT】深度学习Transformer编码器+解码器大白话拆解 图像处理/自然语言通用详解
图像处理·深度学习·学习