想要理解线程池其实很简单

摘要:从一次"创建10万个线程导致服务器宕机"的乌龙事件出发,深度剖析线程池的核心原理与最佳实践。通过手写50行代码实现简易版线程池、7个核心参数的真实案例、以及4种拒绝策略的选型对比,揭秘为什么线程不是越多越好、如何设置线程池大小、以及ThreadPoolExecutor的工作流程。配合时序图展示任务提交流程,给出线程池使用的10条军规。


💥 翻车现场

周二下午,哈吉米写了一个"批量导入用户"的功能。

// 导入10万个用户
public void importUsers(List<User> users) {
    for (User user : users) {
        // 每个用户创建一个线程处理
        new Thread(() -> {
            saveUser(user);
        }).start();
    }
}

哈吉米:"多线程并发处理,应该很快!"

点击"导入"按钮后……

5秒后:
服务器CPU:100%
内存:爆满
应用:卡死

错误日志:
OutOfMemoryError: unable to create new native thread

哈吉米:"卧槽,服务器宕机了!"

紧急重启后,南北绿豆和阿西噶阿西赶来了。

南北绿豆:"你创建了10万个线程?服务器不宕机才怪!"
哈吉米:"线程不是越多越好吗?"
阿西噶阿西:"大错特错!线程是有开销的,而且操作系统能支持的线程数有限!"
南北绿豆:"来,我给你讲讲为什么需要线程池。"


🤔 为什么需要线程池?

问题1:线程创建和销毁的开销

// 每次创建新线程
new Thread(() -> {
    saveUser(user);  // 执行0.1秒
}).start();

// 开销:
// 创建线程:1ms
// 执行任务:0.1秒
// 销毁线程:1ms
// 总耗时:0.1秒 + 2ms

// 如果创建10万个线程:
// 创建+销毁:10万 × 2ms = 200秒
// 任务执行:10万 × 0.1秒 = 10000秒
// 总耗时:10200秒

// 用线程池(复用线程):
// 创建线程:100个 × 1ms = 0.1秒
// 执行任务:10万 × 0.1秒 = 10000秒(并发100个,实际100秒)
// 总耗时:100秒

性能提升:100

阿西噶阿西:"线程池通过复用线程,避免频繁创建和销毁。"


问题2:线程数量失控

操作系统限制:
- Linux默认:单个进程最多创建1024个线程
- Windows:受内存限制

创建10万个线程:
- 每个线程栈空间:1MB
- 总内存:10万 × 1MB = 100GB
- 结果:OutOfMemoryError

南北绿豆:"线程池可以限制线程数量,防止资源耗尽。"


问题3:无法管理和监控

// 创建线程后,无法知道:
// - 有多少线程在运行?
// - 有多少任务在等待?
// - 任务执行成功还是失败?

new Thread(() -> {
    saveUser(user);
}).start();  // 启动后就不管了

南北绿豆:"线程池提供了管理和监控能力。"


🎯 线程池的核心原理

ThreadPoolExecutor的结构

public class ThreadPoolExecutor {
    
    // 核心组件
    private final BlockingQueue<Runnable> workQueue;  // 任务队列
    private final HashSet<Worker> workers;            // 工作线程集合
    private volatile int corePoolSize;                // 核心线程数
    private volatile int maximumPoolSize;             // 最大线程数
    private volatile long keepAliveTime;              // 空闲线程存活时间
    private volatile RejectedExecutionHandler handler;// 拒绝策略
    
    // Worker(工作线程)
    private final class Worker implements Runnable {
        final Thread thread;
        Runnable firstTask;
        
        public void run() {
            while (task != null || (task = getTask()) != null) {
                task.run();  // 执行任务
            }
        }
    }
}

线程池的工作流程

graph TD
    A[提交任务] --> B{当前线程数 < corePoolSize?}
    
    B -->|是| C[创建核心线程执行]
    B -->|否| D{任务队列是否满?}
    
    D -->|否| E[任务加入队列]
    D -->|是| F{当前线程数 < maximumPoolSize?}
    
    F -->|是| G[创建非核心线程执行]
    F -->|否| H[执行拒绝策略]
    
    C --> I[任务执行]
    E --> J[空闲线程从队列取任务]
    G --> I
    J --> I
    
    style C fill:#90EE90
    style E fill:#ADD8E6
    style G fill:#FFE4B5
    style H fill:#FFB6C1

关键流程

1. 线程数 < corePoolSize → 创建核心线程
2. 线程数 >= corePoolSize → 任务加入队列
3. 队列满了 + 线程数 < maximumPoolSize → 创建非核心线程
4. 队列满了 + 线程数 >= maximumPoolSize → 拒绝任务

时序图演示

sequenceDiagram
    participant Client as 客户端
    participant Pool as 线程池
    participant Queue as 任务队列
    participant CoreThread as 核心线程
    participant NonCoreThread as 非核心线程

    Note over Pool: corePoolSize=2, maxPoolSize=5, queueSize=3
    
    Client->>Pool: 1. 提交任务1
    Pool->>CoreThread: 创建核心线程1 ✅
    
    Client->>Pool: 2. 提交任务2
    Pool->>CoreThread: 创建核心线程2 ✅
    
    Client->>Pool: 3. 提交任务3
    Pool->>Queue: 加入队列[task3] ✅
    
    Client->>Pool: 4. 提交任务4
    Pool->>Queue: 加入队列[task3, task4] ✅
    
    Client->>Pool: 5. 提交任务5
    Pool->>Queue: 加入队列[task3, task4, task5] ✅
    
    Client->>Pool: 6. 提交任务6
    Note over Pool: 队列满了,创建非核心线程
    Pool->>NonCoreThread: 创建非核心线程3 ✅
    
    Client->>Pool: 7. 提交任务7
    Pool->>NonCoreThread: 创建非核心线程4 ✅
    
    Client->>Pool: 8. 提交任务8
    Pool->>NonCoreThread: 创建非核心线程5 ✅
    
    Client->>Pool: 9. 提交任务9
    Note over Pool: 队列满 + 线程数达到max
    Pool->>Client: 拒绝任务 ❌

🎯 7个核心参数详解

参数详解

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    int corePoolSize,              // 核心线程数
    int maximumPoolSize,           // 最大线程数
    long keepAliveTime,            // 空闲线程存活时间
    TimeUnit unit,                 // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory,   // 线程工厂
    RejectedExecutionHandler handler    // 拒绝策略
);

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

定义:线程池维持的最少线程数,即使空闲也不会销毁。

示例

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,  // corePoolSize=5
    ...
);

// 提交3个任务 → 创建3个核心线程
// 任务执行完 → 3个核心线程保持活跃(不销毁)

如何设置?

CPU密集型任务:
corePoolSize = CPU核心数 + 1

IO密集型任务:
corePoolSize = CPU核心数 × 2

示例:8核CPU
- CPU密集:corePoolSize = 9
- IO密集:corePoolSize = 16

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

定义:线程池允许创建的最大线程数。

何时创建?

只有当:
1. 任务队列满了
2. 当前线程数 < maximumPoolSize

才会创建非核心线程

示例

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,   // corePoolSize=2
    5,   // maximumPoolSize=5(最多5个线程)
    60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(3)  // 队列容量3
);

// 提交6个任务:
// 任务1、2 → 创建核心线程1、2
// 任务3、4、5 → 加入队列[3, 4, 5]
// 任务6 → 队列满,创建非核心线程3

// 提交第7、8个任务 → 创建非核心线程4、5
// 提交第9个任务 → 拒绝(线程数=5,队列满)

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

定义:非核心线程空闲多久后被销毁。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 5,
    60L, TimeUnit.SECONDS,  // 非核心线程空闲60秒后销毁
    ...
);

// 高峰期:5个线程都在工作
// 低峰期:任务减少,非核心线程空闲60秒后自动销毁,剩下2个核心线程

阿西噶阿西:"这样可以动态伸缩,高峰期多线程,低峰期少线程。"


参数4:workQueue(任务队列)

3种常用队列

队列类型容量特点适用场景
ArrayBlockingQueue有界数组实现,先进先出资源可控
LinkedBlockingQueue可选有界/无界链表实现⭐⭐⭐⭐⭐ 常用
SynchronousQueue0不存储任务,直接交给线程任务立即执行

示例

// 有界队列(推荐)
new ArrayBlockingQueue<>(1000);  // 最多1000个任务

// 无界队列(危险)
new LinkedBlockingQueue<>();  // 无限大,可能OOM

// 同步队列
new SynchronousQueue<>();  // 不缓存任务,直接给线程执行

参数5:threadFactory(线程工厂)

作用:自定义线程的创建方式。

ThreadFactory factory = new ThreadFactory() {
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setName("my-pool-thread-" + threadNumber.getAndIncrement());
        t.setDaemon(false);  // 非守护线程
        t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
};

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, 10, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100),
    factory  // 自定义线程工厂
);

好处

  • ✅ 线程有意义的名称(方便排查问题)
  • ✅ 可以设置优先级、守护线程等

参数6-7:拒绝策略

4种拒绝策略

策略行为适用场景
AbortPolicy(默认)抛出异常需要感知拒绝
CallerRunsPolicy调用者线程执行重要任务,不能丢
DiscardPolicy静默丢弃不重要的任务
DiscardOldestPolicy丢弃队列头的任务保留最新任务

示例

// AbortPolicy(默认)
executor.execute(task);  
// 拒绝时抛异常:RejectedExecutionException

// CallerRunsPolicy
new ThreadPoolExecutor(
    2, 5, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(3),
    new ThreadPoolExecutor.CallerRunsPolicy()  // 调用者执行
);

// 效果:
executor.execute(task);  // 队列满了
// 当前线程(main线程)执行task

🎯 手写一个简易版线程池

哈吉米:"能不能自己实现一个线程池?"

南北绿豆:"来,50行代码实现核心功能!"

/**
 * 简易版线程池(理解原理)
 */
public class SimpleThreadPool {
    
    // 任务队列
    private final BlockingQueue<Runnable> workQueue;
    
    // 工作线程列表
    private final List<WorkerThread> workers = new ArrayList<>();
    
    // 线程池大小
    private final int poolSize;
    
    // 是否关闭
    private volatile boolean isShutdown = false;
    
    public SimpleThreadPool(int poolSize, int queueSize) {
        this.poolSize = poolSize;
        this.workQueue = new ArrayBlockingQueue<>(queueSize);
        
        // 创建工作线程
        for (int i = 0; i < poolSize; i++) {
            WorkerThread worker = new WorkerThread("worker-" + i);
            workers.add(worker);
            worker.start();
        }
    }
    
    /**
     * 提交任务
     */
    public void execute(Runnable task) {
        if (isShutdown) {
            throw new IllegalStateException("线程池已关闭");
        }
        
        // 加入队列(阻塞,队列满时等待)
        try {
            workQueue.put(task);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    /**
     * 关闭线程池
     */
    public void shutdown() {
        isShutdown = true;
        // 中断所有工作线程
        for (WorkerThread worker : workers) {
            worker.interrupt();
        }
    }
    
    /**
     * 工作线程
     */
    private class WorkerThread extends Thread {
        
        public WorkerThread(String name) {
            super(name);
        }
        
        @Override
        public void run() {
            while (!isShutdown) {
                try {
                    // 从队列取任务(阻塞,队列空时等待)
                    Runnable task = workQueue.take();
                    // 执行任务
                    task.run();
                } catch (InterruptedException e) {
                    // 线程池关闭,退出循环
                    break;
                }
            }
        }
    }
}

测试代码

public class SimpleThreadPoolTest {
    
    public static void main(String[] args) {
        // 创建线程池:5个线程,队列容量10
        SimpleThreadPool pool = new SimpleThreadPool(5, 10);
        
        // 提交20个任务
        for (int i = 0; i < 20; i++) {
            final int taskId = i;
            pool.execute(() -> {
                System.out.println("执行任务" + taskId + ",线程:" + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);  // 模拟耗时操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        // 等待5秒后关闭
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        pool.shutdown();
    }
}

输出

执行任务0,线程:worker-0
执行任务1,线程:worker-1
执行任务2,线程:worker-2
执行任务3,线程:worker-3
执行任务4,线程:worker-4
执行任务5,线程:worker-0  ← 复用了worker-0
执行任务6,线程:worker-1  ← 复用了worker-1
...

哈吉米:"原来线程池就是:固定数量的线程 + 任务队列 + 不断取任务执行!"


🎯 如何设置线程池大小?

CPU密集型任务

CPU密集型:大量计算,少量IO

示例:
- 加密解密
- 数据压缩
- 图像处理

推荐线程数:
线程数 = CPU核心数 + 1

原因:CPU密集型,线程太多会频繁上下文切换,反而变慢

测试

线程数执行时间
4(CPU核心数)10秒
810.5秒
1612秒
3215秒(上下文切换开销)

IO密集型任务

IO密集型:大量IO操作,少量计算

示例:
- 数据库查询
- HTTP请求
- 文件读写

推荐线程数:
线程数 = CPU核心数 × 2(或更多)

原因:IO等待时,线程阻塞,CPU空闲,可以增加线程数

更精确的公式

线程数 = CPU核心数 × (1 + IO耗时 / CPU耗时)

示例:
- CPU核心数:8
- IO耗时:80ms
- CPU耗时:20ms

线程数 = 8 × (1 + 80 / 20) = 8 × 5 = 40

混合型任务

拆分成CPU密集型和IO密集型,用不同线程池处理

示例:
cpuPool = new ThreadPoolExecutor(9, 9, ...);  // CPU密集
ioPool = new ThreadPoolExecutor(40, 40, ...);  // IO密集

🛡️ 线程池使用的10条军规

军规1:不要用Executors创建线程池

// ❌ 错误(OOM风险)
ExecutorService executor = Executors.newFixedThreadPool(10);

// 底层实现:
new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>());  // 无界队列,可能OOM

// ✅ 正确
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10, 20, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1000),  // 有界队列
    new ThreadPoolExecutor.CallerRunsPolicy()
);

原因:Executors创建的线程池队列是无界的,可能OOM。


军规2:合理设置队列大小

// ❌ 队列太小(频繁拒绝)
new ArrayBlockingQueue<>(10);

// ❌ 队列太大(OOM)
new ArrayBlockingQueue<>(1000000);

// ✅ 合理大小
new ArrayBlockingQueue<>(1000);

军规3:必须设置拒绝策略

// ✅ 推荐CallerRunsPolicy
new ThreadPoolExecutor.CallerRunsPolicy();

// 效果:任务由调用者线程执行,自然降速

军规4:给线程池起有意义的名字

ThreadFactory factory = new ThreadFactoryBuilder()
    .setNameFormat("order-pool-%d")
    .build();

军规5:监控线程池状态

// 定期打印线程池状态
@Scheduled(fixedDelay = 10000)
public void monitorThreadPool() {
    log.info("活跃线程数: {}", executor.getActiveCount());
    log.info("队列任务数: {}", executor.getQueue().size());
    log.info("已完成任务数: {}", executor.getCompletedTaskCount());
}

军规6:任务要捕获异常

// ❌ 错误
executor.execute(() -> {
    processOrder(order);  // 可能抛异常,导致线程挂掉
});

// ✅ 正确
executor.execute(() -> {
    try {
        processOrder(order);
    } catch (Exception e) {
        log.error("任务执行失败", e);
    }
});

军规7-10(快速总结)

  1. 不要在锁内提交任务(可能死锁)
  2. 及时关闭线程池(shutdown)
  3. 区分CPU密集和IO密集任务
  4. 压测验证线程池参数

🎓 面试标准答案

题目:线程池的工作原理是什么?

答案

核心组件

  1. 核心线程(corePoolSize)
  2. 任务队列(workQueue)
  3. 最大线程(maximumPoolSize)
  4. 拒绝策略(handler)

工作流程

  1. 线程数 < corePoolSize → 创建核心线程
  2. 线程数 >= corePoolSize → 任务加入队列
  3. 队列满 + 线程数 < maxPoolSize → 创建非核心线程
  4. 队列满 + 线程数 >= maxPoolSize → 拒绝任务

线程池大小设置

  • CPU密集:CPU核心数 + 1
  • IO密集:CPU核心数 × 2

🎉 结束语

晚上10点,哈吉米把代码改成了线程池。

哈吉米:"用线程池后,导入10万用户从300秒降到100秒,而且不会OOM了!"

南北绿豆:"对,线程池复用线程,避免频繁创建销毁。"

阿西噶阿西:"记住:不要直接new Thread,用线程池管理线程。"

哈吉米:"还有线程数不是越多越好,要根据任务类型设置!"

南北绿豆:"对,CPU密集型少线程,IO密集型多线程!"


记忆口诀

线程池复用线程快,核心参数要记牢
core先创max后到,队列满了才扩容
CPU密集核心数,IO密集翻一倍
拒绝策略要设置,监控状态别忘了
别用Executors有风险,手动创建才安全


希望这篇文章能帮你轻松理解线程池的原理!记住:理解了线程池,就理解了并发编程的核心思想!💪