一、什么是线程池?
二、线程池的分类
三、线程池的使用
四、ThreadPoolExecutor详解
一、什么是线程池?
线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。
线程池的优势主要体现在以下 4 点:
降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
提高响应速度:任务到达时,无需等待线程创建即可立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
同时阿里巴巴在其《Java开发手册》中也强制规定:线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。
二、线程池的分类
线程池的创建方法总共有 7 种,但总体来说可分为 2 类:
Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。
三、线程池的使用
newFixedThreadPool
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
使用newFixedThreadPool 创建的线程池不会自动关闭。如果你不再需要线程池,应该调用 shutdown 方法来关闭它。shutdown 方法会启动线程池的关闭序列,线程池将不再接受新的任务,但是会等待所有已提交的任务执行完毕。如果你希望等待所有任务完成并检查是否所有任务都成功完成,可以调用 awaitTermination 方法。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小为 5 的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务到线程池执行
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("" + i);
executor.execute(worker); // 将任务添加到线程池进行执行
}
executor.shutdown(); // 关闭线程池,不再接受新的任务,但会等待所有任务完成
// 注意:通常情况下,我们会调用 awaitTermination 方法来等待所有任务完成
// 并且检查是否所有任务都成功完成
while (!executor.isTerminated()) {
}
System.out.println("所有任务已完成");
}
}
class WorkerThread implements Runnable {
private String command;
public WorkerThread(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始. 命令 = " + command);
processCommand();
System.out.println(Thread.currentThread().getName() + " 结束.");
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return this.command;
}
}
newCachedThreadPool
当提交一个新任务到线程池时,如果线程池当前大小小于 corePoolSize(对于 newCachedThreadPool,这个值实际上是 0),则会立即创建新线程来执行任务。如果线程池中的线程都处于空闲状态并且等待队列也为空,那么超过 keepAliveTime(默认为 60 秒)未使用的线程将被终止。因此,线程池中的线程数量将随着需求的变化而动态调整。
这种线程池适用于执行大量短时间的异步任务,因为它可以减少线程上下文切换的开销,并且在需要时可以快速创建新线程。然而,由于它可能会创建大量的线程,所以不适合用于执行长时间运行的任务,因为这可能导致资源耗尽。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
// 创建一个可缓存的线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 提交任务到线程池执行
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("" + i);
executor.execute(worker); // 将任务添加到线程池进行执行
}
// 关闭线程池,但这通常不是必须的,因为 CachedThreadPool 会自动管理线程的生命周期
// 在实际使用中,通常会在应用关闭时调用 shutdown 或 shutdownNow 方法
// executor.shutdown();
// 主线程等待一段时间,以便观察线程池中的任务执行情况
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 注意:在实际应用中,我们通常不会让主线程这样等待,而是会有其他的同步机制来确保所有任务完成
// 在这里,我们只是简单地等待一段时间以便观察结果
System.out.println("主线程继续执行...");
}
}
newSingleThreadExecutor
单个线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutorExample {
public static void main(String[] args) {
// 创建一个单线程的线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 提交任务到线程池执行
for (int i = 0; i < 10; i++) {
final int taskId = i;
Runnable worker = new Runnable() {
@Override
public void run() {
System.out.println("执行任务 " + taskId + ",由线程 " + Thread.currentThread().getName() + " 执行");
}
};
executor.execute(worker); // 将任务添加到线程池进行执行
}
// 关闭线程池
executor.shutdown();
// 等待所有任务完成
while (!executor.isTerminated()) {
}
System.out.println("所有任务已完成");
}
}
newScheduledThreadPool
它用于创建一个可以执行定时或周期性任务的线程池。该线程池中的每个线程都使用默认的 ThreadFactory 创建一个新线程,并使用默认的 RejectedExecutionHandler 来处理拒绝执行的任务。
newScheduledThreadPool 方法接受一个 int 类型的参数,该参数表示线程池的核心线程数,即线程池中允许同时存在的线程数。即使这些线程处于空闲状态,它们也不会被销毁,除非设置了 allowCoreThreadTimeOut 并为 keepAliveTime 设置了非零值。
通过 ScheduledThreadPoolExecutor,你可以安排任务在未来的某个时间点执行一次,或者定期重复执行。它提供了几个方法来安排任务,如 schedule、scheduleAtFixedRate 和 scheduleWithFixedDelay。
当你不再需要 ScheduledThreadPoolExecutor 时,应该调用 shutdown 或 shutdownNow方法来关闭线程池。这可以帮助释放系统资源,并确保所有正在等待的任务都被适当地处理。如果你不关闭线程池,应用程序可能无法正常退出,因为线程池中的线程可能会阻止 JVM 终止。
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
// 创建一个可以执行定时任务的线程池,包含3个核心线程
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
// 安排一个任务在初始延迟后执行一次
executor.schedule(new Runnable() {
@Override
public void run() {
System.out.println("单次执行任务,由线程 " + Thread.currentThread().getName() + " 执行");
}
}, 5, TimeUnit.SECONDS); // 初始延迟5秒
// 安排一个任务定期执行,首次执行有初始延迟,之后每隔一段时间执行一次
executor.scheduleAtFixedRate(new Runnable() {
private int count = 0;
@Override
public void run() {
System.out.println("周期性执行任务,执行次数 " + count++ + ",由线程 " + Thread.currentThread().getName() + " 执行");
}
}, 1, 2, TimeUnit.SECONDS); // 初始延迟1秒,之后每隔2秒执行一次
// 注意:在实际应用中,你通常会希望在应用程序关闭时关闭线程池
executor.shutdown();
}
}
newSingleThreadScheduledExecutor
创建一个单线程的可以执行延迟任务的线程池。
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class SingleThreadScheduledExecutorExample {
public static void main(String[] args) {
// 创建一个单线程定时任务执行器
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
// 安排一个任务在初始延迟后执行一次
executor.schedule(new Runnable() {
@Override
public void run() {
System.out.println("单次执行任务,由线程 " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 执行");
}
}, 5, TimeUnit.SECONDS); // 初始延迟5秒
// 安排一个任务定期执行,首次执行有初始延迟,之后每隔一段时间执行一次
executor.scheduleAtFixedRate(new Runnable() {
private int count = 0;
@Override
public void run() {
System.out.println("周期性执行任务,执行次数 " + count++ + ",由线程 " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 执行");
}
}, 1, 2, TimeUnit.SECONDS); // 初始延迟1秒,之后每隔2秒执行一次
// 注意:在应用程序关闭时,应该关闭线程池
executor.shutdown();
}
}
newWorkStealingPool
Java 8 引入的一个方法,用于创建一个工作窃取线程池(ForkJoinPool 的一个变种)。这个方法属于 java.util.concurrent.Executors 类。工作窃取线程池的设计目标是提高并行应用程序的吞吐量,并减少线程间的竞争。
工作窃取线程池的主要特点是:
工作窃取:当线程池中的某个线程完成其所有任务时,它会尝试从其他线程的队列中“窃取”任务来执行。这种机制有助于平衡负载,减少线程空闲时间,从而提高整体吞吐量。
ForkJoin 框架:newWorkStealingPool 创建的线程池实际上是一个 ForkJoinPool 实例,它支持使用 ForkJoinTask 来分割和合并任务。ForkJoinTask 是一种可以递归地分割成子任务的任务,当子任务完成时,其结果会被合并到父任务中。
并行度:线程池的并行度(即线程数量)可以通过构造函数参数来指定。如果没有指定并行度,则默认使用 Runtime.getRuntime().availableProcessors() 的值,即系统可用的处理器数量。
资源敏感:工作窃取线程池对系统资源(如线程和内存)的使用相对敏感,因此它通常用于需要大量并行处理但资源有限的环境。
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
public class WorkStealingPoolExample {
public static void main(String[] args) {
// 创建一个工作窃取线程池,使用系统默认的并行度
ForkJoinPool pool = (ForkJoinPool) Executors.newWorkStealingPool();
// 提交一个任务给线程池执行
pool.submit(new RecursiveAction() {
@Override
protected void compute() {
// 这里是任务的实现逻辑
// 如果任务可以进一步分割,可以调用 fork() 方法来创建子任务
// 当子任务完成时,会自动调用 join() 方法来合并结果
System.out.println("Task executed by thread " + Thread.currentThread().getName());
}
});
// 关闭线程池(可选,通常在所有任务完成后调用)
// pool.shutdown();
}
}
newWorkStealingPool 创建的线程池主要用于与 ForkJoinTask 配合使用,如果你只是需要执行普通的 Runnable 或 Callable 任务,那么使用 newFixedThreadPool 或 newCachedThreadPool 可能更合适。同时,确保在使用完线程池后,通过调用 shutdown 或 shutdownNow 方法来释放资源。
四、ThreadPoolExecutor详解
提供了一个可扩展的线程池实现。通过ThreadPoolExecutor,你可以控制线程池的基本行为,包括核心线程数、最大线程数、线程空闲时间、任务队列类型以及拒绝策略等。
阿里巴巴《Java开发手册》:
【强制要求】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
corePoolSize:线程池中的核心线程数,即使这些线程空闲,也不会被销毁。除非设置了allowCoreThreadTimeOut。
maximumPoolSize:线程池允许的最大线程数。当队列满了且当前线程数小于这个数时,会创建新线程来处理任务。
keepAliveTime:当线程数大于核心线程数时,这是多余的空闲线程在终止前等待新任务的最长时间。
unit:keepAliveTime 参数的时间单位。
workQueue:用于保存等待执行的任务的队列。此队列仅保存实现了 Runnable 接口的任务。
threadFactory:用于创建新线程的线程工厂。
handler:当 ThreadPoolExecutor 已经关闭或饱和时(达到了最大线程数且工作队列已满),此处理程序用于处理无法执行的任务。
import java.util.concurrent.*;
public class ThreadPoolExecutorExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 60L;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler);
// 提交任务到线程池执行
for (int i = 0; i < 20; i++) {
Runnable worker = new WorkerThread("" + i);
executor.execute(worker); // 将任务添加到线程池的工作队列中
}
// 关闭线程池(可选,通常在所有任务完成后调用)
executor.shutdown(); // 不再接受新任务,等待所有任务执行完毕
// 如果想要立即停止所有任务(包括正在执行的任务),可以使用 executor.shutdownNow();
}
static class WorkerThread implements Runnable {
private String command;
public WorkerThread(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始执行命令 : " + command);
processCommand();
System.out.println(Thread.currentThread().getName() + " 结束执行命令");
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return this.command;
}
}
}
参考:
1、原文链接:https://blog.youkuaiyun.com/m0_46638350/article/details/130925464
2、文言一心