基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

java的线程池

Java 中的线程池(Thread Pool)是多线程编程中极其重要的概念。它可以帮助我们管理和复用线程,避免频繁创建和销毁线程带来的性能开销。

下面我将从基本概念、核心参数、工作原理、常用类型、拒绝策略、以及最佳实践这几个方面,为你系统地介绍 Java 线程池。


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

  1. 降低资源消耗:通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
  2. 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

二、 线程池的核心实现:ThreadPoolExecutor

Java 中的线程池核心实现类是 java.util.concurrent.ThreadPoolExecutor。它最长的一个构造方法包含 7 个核心参数(面试必考):

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. 拒绝策略
)

7 个参数详解:

  1. corePoolSize(核心线程数):线程池中常驻的线程数量。即使这些线程处于空闲状态,也不会被销毁(除非设置了 allowCoreThreadTimeOut)。
  2. maximumPoolSize(最大线程数):线程池允许创建的最大线程数量。当任务队列满了之后,线程池会继续创建线程,直到达到这个值。
  3. keepAliveTime(空闲线程存活时间):当线程数大于核心线程数时,多余的空闲线程存活的时间。
  4. unit(时间单位)keepAliveTime 的时间单位(如 TimeUnit.SECONDS)。
  5. workQueue(任务队列):用于存放等待执行的任务的阻塞队列。常见的有:
    • LinkedBlockingQueue(无界/有界链表队列)
    • ArrayBlockingQueue(有界数组队列)
    • SynchronousQueue(不存储元素的阻塞队列)
  6. threadFactory(线程工厂):用于创建新线程,可以给线程起一个有意义的名字,方便排查问题。
  7. handler(拒绝策略):当线程池和队列都满了,新提交的任务该如何处理。

三、 线程池的工作原理(工作流程)

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

  1. 核心线程未满:如果当前运行的线程数小于 corePoolSize,则直接创建新线程(核心线程)来执行任务。
  2. 队列未满:如果当前运行的线程数达到 corePoolSize,新任务会被放入任务队列 workQueue 中等待。
  3. 最大线程未满:如果任务队列已满,且当前运行的线程数小于 maximumPoolSize,则创建非核心线程来执行任务。
  4. 触发拒绝策略:如果队列已满,且运行的线程数已达到 maximumPoolSize,则触发 RejectedExecutionHandler 拒绝策略。

比喻记忆法
线程池就像一家外包公司。

  • corePoolSize正式员工
  • workQueue需求积压池(任务队列)
  • maximumPoolSize 是正式员工 + 临时工(非核心线程)的总和。
  • 当正式员工忙不过来,需求先放积压池;积压池满了,公司赶紧招临时工;临时工也招满了,再来新需求,只能拒绝(拒绝策略)

四、 四种常见的拒绝策略

当线程池无力处理新任务时,Java 提供了 4 种内置策略:

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

五、 常见的四种内置线程池(通过 Executors 创建)

Java 在 Executors 工具类中提供了几种快速创建线程池的方法,但不推荐在生产环境中使用(原因见后文):

  1. CachedThreadPool(可缓存线程池)
    • 特点:核心线程数为0,最大线程数为 Integer.MAX_VALUE。来一个任务就建一个线程。
    • 风险:可能导致创建大量线程,导致 OOM(内存溢出)
  2. FixedThreadPool(定长线程池)
    • 特点:核心线程数等于最大线程数。
    • 风险:使用的是无界队列 LinkedBlockingQueue,任务堆积过多会导致 OOM
  3. SingleThreadExecutor(单线程池)
    • 特点:只有一个核心线程。
    • 风险:同样使用无界队列,存在 OOM 风险。
  4. ScheduledThreadPool(定时线程池)
    • 特点:支持定时及周期性任务执行。

⚠️ 阿里《Java开发手册》强制规定
线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
并且,线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。


六、 线程池的最佳实践与配置

1. 如何合理配置线程池大小?

线程池大小的设置取决于任务的类型:

  • CPU 密集型任务(如:加密、计算、视频解码):
    • 线程主要消耗 CPU 资源,线程数不宜过多。
    • 推荐:CPU 核心数 + 1N_cpu + 1)。多出的一个线程是为了防止线程偶发性暂停导致 CPU 空闲。
  • I/O 密集型任务(如:数据库读写、网络请求、文件读写):
    • 线程大部分时间在等待 I/O 操作,CPU 比较空闲。
    • 推荐:2 * CPU 核心数2 * N_cpu),或者使用公式:CPU 核心数 * (1 + 线程等待时间 / 线程计算时间)

2. 代码示例(推荐的创建方式)

java
import java.util.concurrent.*;

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

        // 手动创建线程池
        ExecutorService threadPool = new ThreadPoolExecutor(
                cpuCores,                      // 核心线程数
                cpuCores * 2,                  // 最大线程数
                60L,                           // 空闲存活时间
                TimeUnit.SECONDS,              // 时间单位
                new LinkedBlockingQueue<>(500),// 有界队列,防止 OOM
                Executors.defaultThreadFactory(), // 线程工厂
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者运行
        );

        // 提交任务
        for (int i = 0; i < 10; i++) {
            final int index = i;
            threadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + index);
            });
        }

        // 关闭线程池(优雅关闭)
        threadPool.shutdown();
    }
}

3. 提交任务的两种方式:execute() vs submit()

  • execute():用于提交不需要返回值的任务。
    java
    threadPool.execute(new Runnable() { ... });
  • submit():用于提交需要返回值的任务。它会返回一个 Future 对象,可以通过 future.get() 获取执行结果(会阻塞直到任务完成)。
    java
    Future<String> future = threadPool.submit(new Callable<String>() { ... });
    String result = future.get();

总结

掌握 Java 线程池的关键在于理解 ThreadPoolExecutor 的 7 个参数以及任务排队和执行的流程。在实际开发中,务必手动创建 ThreadPoolExecutor 并指定有界队列,以确保系统的安全与稳定。

00:00
00:00