基于本文回答
0
评论

java线程池

Java 中的线程池(Thread Pool)是多线程编程的核心技术之一。它是一种基于池化思想管理线程的工具,旨在减少创建和销毁线程的开销,提高系统资源的利用率和吞吐量。

以下是关于 Java 线程池的系统性介绍,涵盖其原理、核心参数、工作流程、创建方式及最佳实践


一、 为什么使用线程池?(优势)

在没有线程池之前,每次需要执行异步任务时都需要 new Thread()。这样做存在以下问题:

  1. 开销大:频繁创建和销毁线程会消耗大量的系统资源(内存和 CPU)。
  2. 无序/不可控:线程无限制创建会占满系统内存,导致 OutOfMemoryError (OOM) 甚至系统崩溃。
  3. 缺乏管理:无法提供定时执行、定期执行、线程中断等功能。

使用线程池的好处:

  • 降低资源消耗:重复利用已创建的线程,减少线程创建和销毁的消耗。
  • 提高响应速度:任务到达时,不需要等待线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,使用线程池可以进行统一分配、调优和监控。

二、 线程池的核心子类:ThreadPoolExecutor

Java 中的线程池核心实现类是 java.util.concurrent.ThreadPoolExecutor。理解这个类的 7 个核心参数 是掌握线程池的关键。

1. 核心参数详解(重点)

java
public ThreadPoolExecutor(
    int corePoolSize,               // 1. 核心线程数
    int maximumPoolSize,            // 2. 最大线程数
    long keepAliveTime,             // 3. 空闲线程存活时间
    TimeUnit unit,                  // 4. 时间单位
    BlockingQueue<Runnable> workQueue, // 5. 任务队列
    ThreadFactory threadFactory,    // 6. 线程工厂
    RejectedExecutionHandler handler // 7. 拒绝策略
)
  1. corePoolSize (核心线程数)
    • 线程池中常驻的线程数。即使这些线程处于空闲状态,也不会被销毁(除非设置了 allowCoreThreadTimeOut)。
  2. maximumPoolSize (最大线程数)
    • 线程池允许创建的最大线程数量。当队列满时,会继续创建线程,直到达到这个值。
  3. keepAliveTime (空闲存活时间)
    • 当线程数大于 corePoolSize 时,多余的空闲线程在终止前等待新任务的最长时间。
  4. unit (时间单位)
    • keepAliveTime 的时间单位(如 TimeUnit.SECONDS)。
  5. workQueue (任务队列)
    • 存放待执行任务的阻塞队列。常见的有:
      • ArrayBlockingQueue:基于数组的有界阻塞队列。
      • LinkedBlockingQueue:基于链表的无界(或有界)阻塞队列。
      • SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等待一个移出操作。
  6. threadFactory (线程工厂)
    • 用于创建新线程,可以自定义线程名称、守护进程状态等。
  7. handler (拒绝策略)
    • 当线程池和队列都满了,如何处理新提交的任务。

三、 线程池的工作流程(非常重要)

当一个新任务提交到线程池时,处理流程如下:

  1. 步骤 1:如果当前运行的线程数小于 corePoolSize,则直接创建核心线程来执行任务。
  2. 步骤 2:如果当前运行的线程数大于等于 corePoolSize,则将任务放入任务队列 workQueue
  3. 步骤 3:如果任务队列已满,且当前运行的线程数小于 maximumPoolSize,则创建非核心线程来执行任务。
  4. 步骤 4:如果队列满了,且线程数已经达到了 maximumPoolSize,则触发拒绝策略

记忆口诀:先核心,再队列,后最大,最后拒绝。


四、 拒绝策略 (RejectedExecutionHandler)

JDK 提供了 4 种内置的拒绝策略:

  1. AbortPolicy(默认):直接抛出 RejectedExecutionException 异常,阻止系统正常运行。
  2. CallerRunsPolicy:“调用者运行”机制。用调用者所在的线程(通常是主线程)来执行任务。这会降低新任务的提交速度,起到限流作用。
  3. DiscardPolicy:直接丢弃任务,不予处理也不抛出异常。
  4. DiscardOldestPolicy:丢弃队列中最老的任务(最先进入队列的任务),然后尝试重新提交当前任务。

五、 常见的四种快捷创建线程池(不推荐在生产使用)

Java 的 Executors 类提供了创建几种常用线程池的静态方法:

  1. newCachedThreadPool
    • 特点:可缓存线程池,核心线程数为 0,最大线程数为 Integer.MAX_VALUE
    • 缺点:允许创建的线程数量太多,可能导致 CPU 100% 或 OOM。
  2. newFixedThreadPool(int nThreads)
    • 特点:固定大小的线程池。
    • 缺点:任务队列是无界的(LinkedBlockingQueue),如果任务堆积过多会导致 OOM。
  3. newSingleThreadExecutor
    • 特点:单线程线程池,保证任务按顺序执行。
    • 缺点:同样使用无界队列,存在 OOM 风险。
  4. newScheduledThreadPool
    • 特点:支持定时及周期性任务执行。

⚠️ 【避坑指南】阿里巴巴《Java开发手册》强制规定
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式显式创建。
避免因为默认配置的队列长度为 Integer.MAX_VALUE(无界)或者最大线程数为 Integer.MAX_VALUE 导致内存溢出。


六、 推荐的线程池创建与使用方式

在实际开发中,应该使用自定义的 ThreadPoolExecutor

java
import java.util.concurrent.*;

public class MyThreadPool {
    public static void main(String[] args) {
        // 获取 CPU 核心数
        int cpuCores = Runtime.getRuntime().availableProcessors();

        // 1. 手动创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                cpuCores,                             // 核心线程数
                cpuCores * 2,                         // 最大线程数
                60L, TimeUnit.SECONDS,               // 空闲线程存活时间
                new LinkedBlockingQueue<>(100),       // 有界队列,容量 100
                Executors.defaultThreadFactory(),     // 线程工厂
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        // 2. 提交任务
        for (int i = 0; i < 10; i++) {
            final int index = i;
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + index);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 3. 关闭线程池
        executor.shutdown(); // 优雅关闭,不再接受新任务,等待现有任务执行完
        // executor.shutdownNow(); // 强行关闭,尝试中断正在执行的任务,返回未执行的任务列表
    }
}

七、 线程池的参数如何配置(如何确定线程数)?

线程池的大小配置主要取决于任务的性质,一般分为两类:

1. CPU 密集型任务(如:加密、大数计算、压缩)

  • 特点:主要是 CPU 在进行计算,没有或很少有 I/O 阻塞。
  • 合理线程数CPU 核心数 + 1
  • 原因:CPU 满载运转,再多的线程只会增加线程上下文切换的开销。+1 是为了防止某个线程偶尔暂停(如缺页中断)而导致的 CPU 空闲。

2. I/O 密集型任务(如:网络请求、数据库读写、文件读写)

  • 特点:系统大部分时间在等待 I/O 操作完成,此时 CPU 处于空闲状态。
  • 合理线程数CPU 核心数 * 2,或者更精确的公式:
    线程数=CPU 核心数×(1+线程等待时间线程计算时间)\text{线程数} = \text{CPU 核心数} \times \left(1 + \frac{\text{线程等待时间}}{\text{线程计算时间}}\right)
  • 经验值:在实际 Web 应用(如 Spring Boot)中,通常配置为核心数的 2 到 4 倍(例如 16 或 32),具体需要通过压测微调。

总结

  1. 核心逻辑:线程池优先使用核心线程,其次放进阻塞队列,最后创建非核心线程。
  2. 生产禁用:不要用 Executors 快捷创建,一律用 new ThreadPoolExecutor(...)
  3. 监控与调优:合理设置有界队列大小和拒绝策略,并在线上对线程池的活跃线程数、队列积压数进行监控。
右滑查看面试常问