参考:https://blog.csdn.net/fwt336/article/details/81530581
前言
在开发中为了提高系统的响应速度和处理能力会使用到多线程,但线程的创建和释放,需要占用不小的内存和资源。如果每次需要使用线程时,都new 一个Thread的话,难免会造成资源的浪费,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源导致系统瘫痪。不利于扩展。就像MySQL数据库连接一样,每创建一个连接都需要消耗资源,所以就引入了数据库连接池,线程也引入了线程池的概念,需要线程时可以不用创建,直接从池中获取,在JDK中就为我们提供了ExecutorService。
ExecutorServiceExecutorService 提供了几种不同类型的线程池,包括单线程池、固定大小线程池、可缓存线程池和定时任务线程池。通过这些线程池,我们可以有效地管理多个任务的执行,并且可以控制线程池的大小,可以有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,同时提供定时执行、定期执行、单线程、并发数控制等功能,也不用使用TimerTask了。
1. ExecutorService的创建方式
java
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
所有线程池最终都是通过这个方法来创建的。
corePoolSize : 核心线程数,一旦创建将不会再释放。如果创建的线程数还没有达到指定的核心线程数量,将会继续创建新的核心线程,直到达到最大核心线程数后,核心线程数将不在增加;达到核心线程数但没有空闲的核心线程,同时又未达到最大线程数,则将继续创建非核心线程;如果核心线程数等于最大线程数,则当核心线程都处于激活状态时,任务将被挂起,等待空闲线程来执行。
maximumPoolSize : 最大线程数,允许创建的最大线程数量。如果最大线程数等于核心线程数,则无法创建非核心线程;如果非核心线程处于空闲时,超过设置的空闲时间,则将被回收,释放占用的资源。
keepAliveTime : 也就是当线程空闲时,所允许保存的最大时间,超过这个时间,线程将被释放销毁,但只针对于非核心线程。
unit : 时间单位,TimeUnit.SECONDS等。
workQueue : 任务队列,存储暂时无法执行的任务,等待空闲线程来执行任务。
threadFactory : 线程工程,用于创建线程。
handler : 当线程边界和队列容量已经达到最大时,用于处理阻塞时的程序
2.线程池的类型
2.1 可缓存线程池
java
ExecutorService cachePool = Executors.newCachedThreadPool();
看看它的具体创建方式:
java
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, //核心线程数
Integer.MAX_VALUE, //线程池大小
60L, //空闲保存时间
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>() //阻塞队列);
}
通过它的创建方式可以知道,创建的都是非核心线程,而且最大线程数为Interge的最大值,空闲线程存活时间是1分钟。如果有大量耗时的任务,则不适该创建方式。它只适用于生命周期短的任务。
2.2 单线程池
java
ExecutorService singlePool = Executors.newSingleThreadExecutor();
顾名思义,也就是创建一个核心线程:
java
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
只用一个线程来执行任务,保证任务按FIFO顺序一个个执行。
2.3 固定线程数线程池
java
Executors.newFixedThreadPool(3);
java
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
也就是创建固定数量的可复用的线程数,来执行任务。当线程数达到最大核心线程数,则加入队列等待有空闲线程时再执行。
2.4 固定线程数,支持定时和周期性任务
java
ExecutorService scheduledPool = Executors.newScheduledThreadPool(5);
java
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
可用于替代handler.postDelay和Timer定时器等延时和周期性任务。
问题:最大线程数量为Integer最大值,不就是可以创建很多个空闲线程用来处理任务吗?为什么会任务还会被放入工作队列中等待处理
在 ScheduledThreadPoolExecutor 的构造方法中,将最大线程数量设置为 Integer.MAX_VALUE。这意味着线程池可以创建非常多的空闲线程来处理任务。然而,即使存在大量空闲线程,仍然会将任务放入工作队列中等待处理的原因是为了减少线程创建和销毁的开销,并且能够更好地控制线程的数量。通过工作队列,线程池可以根据任务的到达速率和线程的处理能力来动态调整任务的执行顺序和并发度,以保证任务能够按时得到执行。因此,虽然 ScheduledThreadPoolExecutor 可以创建大量的空闲线程,但为了更好地管理和控制线程数量,任务仍然会被放入工作队列中等待处理,以实现更高效的任务调度和执行。
java
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
java
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
java
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
scheduleAtFixedRate
和sheduleWithFixedDelay
有什么不同呢?
scheduleAtFixedRate: 用于以固定的时间间隔执行任务。它接受一个 Runnable 类型的参数以及两个 long 类型的参数:initialDelay(初始延迟时间)和 period(任务执行的时间间隔)。该方法会在 initialDelay 时间后开始执行第一次任务,然后每隔 period 时间执行一次。如果任务的执行时间超过了指定的时间间隔 period,那么下一次任务的执行会立即开始,不会等待上一次任务的完成。这意味着任务的执行可能会重叠。
sheduleWithFixedDelay: 在任务执行完成后,等待 delay 时间后再开始下一次任务的执行。这样可以确保任务之间有固定的时间间隔,并且不会重叠执行。
2.5 手动创建线程池
java
private ExecutorService pool = new ThreadPoolExecutor(3, 10,
10L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(512), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
可以根据自己的需求创建指定的核心线程数和总线程数。
3. 应用场景
- 并发任务执行:ExecutorService 可以用于执行并发的异步任务,通过线程池的方式,可以有效地管理和复用线程资源,提高任务执行的效率。
- 定时任务调度:ExecutorService 可以用于定时执行任务,通过调用 schedule() 或 scheduleAtFixedRate() 方法,可以实现按照指定的时间间隔或固定频率执行任务的功能。
- 大规模数据处理:当需要对大规模数据进行处理时,可以将数据分割成多个任务,并提交给 ExecutorService 执行,以并发的方式对数据进行处理,提高处理速度。
4. 代码案例
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentTaskExample {
public static void main(String[] args) {
// 创建固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 提交 10 个任务给线程池执行
for (int i = 0; i < 10; i++) {
int taskId = i;
executorService.submit(() -> {
System.out.println("任务 " + taskId + " 开始执行");
// 模拟任务执行耗时
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务 " + taskId + " 执行完成");
});
}
// 关闭线程池
executorService.shutdown();
try {
// 等待所有任务完成或超时(这里设置超时时间足够大)
if (!executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)) {
// 如果超时仍有任务未完成,则强制关闭线程池
executorService.shutdownNow();
System.out.println("等待超时,强制关闭线程池");
}
} catch (InterruptedException e) {
// 捕获中断异常
e.printStackTrace();
executorService.shutdownNow();
}
}
}
结果任务不是按进来无序完成的,任务1耗时并不会阻塞程序对任务2的处理。
5. 注意点
在上面的代码中需要关闭线程池,线程池的关闭可以确保线程池中的线程在不再需要时被正确地释放和销毁。不关闭线程池可能在某些情况下工作正常,但它可能导致一些潜在问题:
-
资源泄漏: 如果不关闭线程池,它将一直保持活动状态,并且线程池中的线程将继续存在。这会导致资源的浪费,特别是在长时间运行的应用程序中。
-
系统负载: 线程池中的线程将占用系统资源,包括内存和处理器。如果不关闭线程池,这些资源将被持续占用,可能导致系统负载增加。
-
程序退出延迟: 如果主程序不关闭线程池并等待线程池中的任务完成,那么程序可能会在所有任务完成之前退出。这可能导致任务未执行完全或结果未被处理。所以代码中设置了一个足够大的超时时间,确保任务完全执行完。
因此,为了避免以上问题,建议在不再需要线程池时,显式地调用 shutdown() 方法来关闭线程池。这将停止线程池接受新的任务,并开始逐渐关闭线程池中的线程,直到所有任务都执行完毕。
之前部署过一个SpringBoot项目在云服务器上,总是运行一阵子服务器就会宕机,内存占满了,但是这个项目只有我一个人在用,不存在Redis占用过高内存的问题,后来排查到是我使用线程池后没有关闭,导致了系统资源耗尽。即使线程池的线程数是固定的,也会造成资源的消耗。