线程池原理
线程池是一种线程复用的技术,它主要用于控制运行的线程的数量,通过将任务放入队列,然后在线程创建后启动这些任务。当线程数量超过最大限制时,超出数量的线程会排队等候,直到其他线程执行完毕,再从队列中取出任务来执行。这种机制有效地降低了资源消耗,提高了响应速度,并增强了线程的可管理性。
线程池的组成
线程池主要由以下几个部分组成:
-
线程池管理器:用于创建并管理线程池。它负责初始化线程池,设置核心线程数、最大线程数等参数,并根据任务队列和任务执行情况动态调整线程池中的线程数量。
-
工作线程:线程池中的线程,负责执行任务。当线程池接收到新任务时,工作线程会从任务队列中取出任务并执行。
-
任务接口:每个任务必须实现的接口,用于工作线程调度其运行。这通常是一个 Runnable 或 Callable 接口,任务通过实现这些接口中的 run 或 call 方法来定义自己的执行逻辑。
-
任务队列:用于存放待处理的任务,提供一种缓冲机制。当线程池中的线程数达到上限时,新任务会被添加到任务队列中等待执行。任务队列可以是阻塞队列,也可以是其他类型的队列,具体取决于线程池的配置和需求。
在 Java 中,线程池是通过 Executor 框架实现的。该框架提供了一组用于创建、调度和管理线程池的类和接口,包括 Executor、Executors、ExecutorService、ThreadPoolExecutor 等。其中,ThreadPoolExecutor 是线程池的核心实现类,它允许用户通过构造函数或 setter 方法来配置线程池的各种参数。
ThreadPoolExecutor 的构造方法通常包含以下参数:
- corePoolSize:线程池中的核心线程数量。
- maximumPoolSize:线程池中的最大线程数量。
- keepAliveTime:当线程池中的线程数量超过 corePoolSize 时,多余的空闲线程的存活时间。
- unit:keepAliveTime 的时间单位。
- workQueue:任务队列,用于存放待执行的任务。
- threadFactory:线程工厂,用于创建新的线程。
- handler:拒绝策略,当线程池无法处理新任务时,所采取的拒绝策略。
线程池的拒绝策略
当线程池中的线程数量达到上限,且任务队列已满时,线程池需要采取拒绝策略来处理新任务。JDK 提供了几种内置的拒绝策略,用户也可以根据需要实现自己的拒绝策略。
-
AbortPolicy:这是线程池默认的拒绝策略。当任务被拒绝时,它会抛出 RejectedExecutionException 异常。这种策略适用于那些不允许任务丢失的关键业务场景。
-
CallerRunsPolicy:当任务被拒绝时,该策略会将任务回退到调用者线程中执行。这种策略不会丢弃任务,但可能会导致调用者线程的性能下降。
-
DiscardOldestPolicy:当任务被拒绝时,该策略会丢弃任务队列中最老的任务(即等待时间最长的任务),并尝试再次提交当前任务。这种策略可能会导致某些任务永远得不到执行,但通常用于那些允许任务丢失的非关键业务场景。
-
DiscardPolicy:当任务被拒绝时,该策略会默默地丢弃任务,不做任何处理。这种策略适用于那些允许任务丢失且对业务影响不大的场景。
线程的复用
线程复用是指线程执行完一个任务后不立即销毁,而是等待执行下一个任务。这种机制有效地降低了线程的创建和销毁开销,提高了系统的性能。在线程池中,工作线程在执行完一个任务后会自动从任务队列中取出下一个任务来执行,从而实现线程的复用。
线程池工作的详细过程
线程池的工作过程可以分为以下几个步骤:
-
初始化线程池:在创建线程池时,会指定核心线程数、最大线程数、任务队列容量等参数。线程池管理器会根据这些参数来初始化线程池。
-
接收任务:当有新任务提交到线程池时,线程池会首先判断当前正在运行的线程数量是否小于核心线程数。如果是,则立即创建一个新的工作线程来执行任务;否则,将任务添加到任务队列中等待执行。
-
执行任务:工作线程会从任务队列中取出任务并执行。如果任务队列为空,且当前正在运行的线程数量小于最大线程数,则线程池会创建一个新的工作线程来执行任务;如果任务队列为空,且当前正在运行的线程数量已达到最大线程数,则工作线程会进入等待状态,直到有新的任务添加到任务队列中。
-
回收线程:当一个工作线程完成任务后,它会从任务队列中取出下一个任务来执行。如果任务队列为空,且当前正在运行的线程数量超过了核心线程数,且该工作线程已经空闲超过了指定的存活时间(keepAliveTime),则线程池会回收这个工作线程,将其从线程池中移除。
-
处理拒绝策略:如果线程池中的线程数量已达到最大线程数,且任务队列已满,此时再有新任务提交到线程池时,线程池会采取拒绝策略来处理新任务。具体的拒绝策略取决于线程池的配置和需求。
示例展示一下如何使用 Java 中的 ThreadPoolExecutor 类来创建一个线程池,并提交任务到线程池中执行:
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建一个线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3) // 任务队列
);
// 提交任务到线程池
for (int i = 0; i < 10; i++) {
executor.execute(new Task(i));
}
// 关闭线程池
executor.shutdown();
}
static class Task implements Runnable {
private int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task " + taskId + " is running.");
try {
// 模拟任务执行时间
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed.");
}
}
}
代码中,创建了一个核心线程数为 2、最大线程数为 5、空闲线程存活时间为 60 秒、任务队列容量为 3 的线程池。然后,我们提交了 10 个任务到线程池中执行。由于任务队列的容量限制和线程池的最大线程数限制,一些任务可能会被延迟执行或等待其他任务完成后再执行。
在 ThreadPoolExecutor 类中,execute 方法是提交任务到线程池的关键方法。它的实现逻辑大致如下:
-
检查当前正在运行的线程数量:如果当前正在运行的线程数量小于核心线程数,则立即创建一个新的工作线程来执行任务。
-
将任务添加到任务队列中:如果当前正在运行的线程数量大于或等于核心线程数,则尝试将任务添加到任务队列中。如果任务队列已满,则进入下一步。
-
创建非核心线程执行任务:如果任务队列已满,且当前正在运行的线程数量小于最大线程数,则创建一个非核心线程来执行任务。
-
处理拒绝策略:如果任务队列已满,且当前正在运行的线程数量已达到最大线程数,则调用拒绝策略来处理新任务。
在 addWorker 方法中,线程池会创建一个新的工作线程,并将其添加到工作线程集合中。然后,它会启动这个工作线程来执行任务队列中的任务。当工作线程完成任务后,它会从任务队列中取出下一个任务来执行,从而实现线程的复用。
结语
线程池是一种高效的线程管理机制,它通过复用线程、控制最大并发数、管理线程等方式来降低资源消耗、提高响应速度并增强系统的稳定性。在 Java 中,线程池是通过 Executor 框架实现的,用户可以通过配置 ThreadPoolExecutor 类的各种参数来定制线程池的行为。