源本科技 | 码上会

Java 线程池

2026/01/29
34
0

学习目标

  • 理解线程池的基本概念、工作原理及其在并发编程中的价值

  • 掌握线程池的核心优势与典型应用场景

  • 能够使用 Java 标准库(如 ExecutorService)或自定义方式创建和管理线程池

  • 了解线程池的生命周期控制方法(如 shutdown()


什么是线程池?

线程池(Thread Pool)是一种用于管理和复用线程的并发设计模式。它预先创建一组工作线程,并将它们保持在“待命”状态,以执行提交的任务。与每次任务都新建线程相比,线程池避免了频繁创建和销毁线程带来的性能开销资源浪费

现实类比:呼叫中心

想象一个拥有 10 名客服代表的呼叫中心:

  • 当客户来电时,空闲的客服立即接听;

  • 如果所有客服都在忙,新来电会进入等待队列;

  • 客服完成一次通话后不会离职,而是继续处理下一个来电。

这正是线程池的工作方式:固定数量的工作者 + 任务队列 + 复用机制


线程池优势

优势

说明

性能提升

避免线程创建 / 销毁的系统开销(上下文切换、内存分配等)

响应更快

任务无需等待新线程启动,可立即由空闲线程执行

资源可控

限制最大并发线程数,防止系统因线程爆炸而崩溃(如 OutOfMemoryError

任务排队

超出处理能力的任务可暂存于队列,实现平滑负载缓冲


工作流程

  1. 初始化阶段
    创建固定数量的工作线程(如 3 个),并启动它们。这些线程初始处于空闲状态,等待任务。

  2. 任务提交
    用户通过 submit() 方法将 RunnableCallable 任务放入任务队列。

  3. 任务分配

    • 若有空闲线程,立即取出队列头部任务并执行;

    • 若所有线程忙碌,新任务在队列中等待。

  4. 线程复用
    线程完成当前任务后,不退出,而是回到池中继续从队列取下一个任务。

  5. 优雅关闭
    调用 shutdown() 后,线程池停止接收新任务,但会完成已提交的任务。


自定义线程池

以下是一个简化版的线程池实现,帮助理解其内部机制:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 工作线程:持续从任务队列中取出并执行任务
 */
class Worker extends Thread {
    private final BlockingQueue<Runnable> taskQueue;
    private volatile boolean running = true; // 控制运行状态

    public Worker(BlockingQueue<Runnable> queue, String name) {
        super(name);
        this.taskQueue = queue;
    }

    @Override
    public void run() {
        try {
            while (running) {
                // 阻塞等待任务(若队列为空则挂起)
                Runnable task = taskQueue.take();
                
                // 检查是否收到终止信号(“毒丸” 一种优雅终止消费者线程的经典模式)
                if (task == SimpleThreadPool.POISON_PILL) {
                    System.out.println(getName() + " 收到终止信号,即将退出");
                    break;
                }

                // 执行任务
                task.run();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 恢复中断状态
        }
    }

    /**
     * 请求关闭工作线程(配合 interrupt 唤醒阻塞的 take())
     */
    public void shutdown() {
        running = false;
        interrupt(); // 唤醒可能阻塞在 take() 上的线程
    }
}

/**
 * 简易线程池实现
 */
class SimpleThreadPool {
    // 终止信号:“毒丸”(一个特殊的空任务)
    static final Runnable POISON_PILL = () -> {};

    private final BlockingQueue<Runnable> taskQueue;
    private final Worker[] workers;

    /**
     * 构造线程池
     * @param poolSize 线程数量
     */
    public SimpleThreadPool(int poolSize) {
        this.taskQueue = new LinkedBlockingQueue<>();
        this.workers = new Worker[poolSize];

        // 启动所有工作线程
        for (int i = 0; i < poolSize; i++) {
            workers[i] = new Worker(taskQueue, "工作线程-" + (i + 1));
            workers[i].start();
        }
    }

    /**
     * 提交一个任务到队列
     */
    public void submit(Runnable task) {
        taskQueue.offer(task); // 非阻塞提交
    }

    /**
     * 优雅关闭线程池:
     * 向队列中放入与工作线程数量相同的“毒丸”,通知它们退出
     */
    public void shutdown() {
        for (Worker worker : workers) {
            taskQueue.offer(POISON_PILL);
        }
    }
}

/**
 * 测试简易线程池
 */
public class ThreadPoolDemo {
    public static void main(String[] args) {
        SimpleThreadPool pool = new SimpleThreadPool(3); // 创建含 3 个工作线程的池

        // 提交 5 个任务
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            pool.submit(() -> {
                System.out.println("正在执行任务 " + taskId + 
                                   ",由 " + Thread.currentThread().getName() + " 处理");
                try {
                    Thread.sleep(1000); // 模拟耗时操作
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 关闭线程池
        pool.shutdown();
        
        // 可选:等待所有线程结束(非必须,但便于观察输出)
        for (Thread worker : pool.workers) {
            try {
                worker.join();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        System.out.println("线程池已完全关闭");
    }
}

可能的输出:

正在执行任务 1,由 工作线程-1 处理
正在执行任务 2,由 工作线程-2 处理
正在执行任务 3,由 工作线程-3 处理
正在执行任务 4,由 工作线程-1 处理
正在执行任务 5,由 工作线程-2 处理
工作线程-3 收到终止信号,即将退出
工作线程-1 收到终止信号,即将退出
工作线程-2 收到终止信号,即将退出
线程池已完全关闭

注意:实际执行顺序可能因线程调度而异,但前三个任务会并行执行,后两个任务在前一批完成后执行。


Java 标准线程池

虽然自定义实现有助于理解原理,但在生产环境中应优先使用 Java 提供的成熟线程池工具——ExecutorService

Executors

方法

说明

newFixedThreadPool(int n)

固定大小线程池,核心线程数 = 最大线程数

newCachedThreadPool()

弹性线程池,空闲线程 60 秒后回收,适合短生命周期任务

newSingleThreadExecutor()

单线程池,保证任务按提交顺序串行执行

newScheduledThreadPool(int n)

支持定时及周期性任务执行

使用示例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class StandardThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 5; i++) {
            int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + 
                                   " executed by " + Thread.currentThread().getName());
            });
        }

        executor.shutdown(); // 不再接受新任务,但会完成已提交任务
    }
}

常用管理方法

方法

作用

submit(Runnable task)

提交任务到队列,由空闲线程执行

shutdown()

优雅关闭:停止接收新任务,等待已提交任务完成

shutdownNow()

强制关闭:尝试中断所有正在执行的任务,并清空队列

isShutdown()

判断是否已调用 shutdown()

isTerminated()

判断所有任务是否已完成且线程池已关闭

awaitTermination(long timeout, TimeUnit unit)

等待线程池完全终止,最多等待指定时间

最佳实践:始终调用 shutdown() 避免程序无法正常退出。


重点总结

  • 线程池通过复用线程显著提升并发性能,避免频繁创建 / 销毁线程的开销。

  • 其核心组件包括:工作线程池任务队列任务调度策略

  • 自定义实现有助于理解原理,但生产环境应使用 ExecutorService 及其工厂方法。

  • 必须正确管理线程池生命周期,尤其是调用 shutdown() 以确保资源释放。

  • 线程池大小应根据任务类型(CPU 密集型 vs I/O 密集型)合理配置,通常:

    • CPU 密集型:线程数 ≈ CPU 核心数

    • I/O 密集型:线程数可远大于 CPU 核心数


思考题

  1. 如果不调用 shutdown(),使用 ExecutorService 的程序会一直运行吗?为什么?

  2. 在高并发 Web 服务器中,为什么 newCachedThreadPool() 可能导致内存溢出?如何避免?

  3. 设计一个支持动态调整线程数量的线程池(如根据 CPU 负载自动扩容 / 缩容),你会考虑哪些关键因素?