线程池(Thread Pool)是一种基于池化思想管理线程的工具,主要用于减少创建和销毁线程的开销。在多线程编程中,频繁地创建和销毁线程会消耗大量系统资源,而线程池可以复用一组已经创建好的线程。
线程池是多线程编程中常用的一种优化手段,可以提高资源利用率,提升系统性能,并降低系统的复杂性。
这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处:
可以通过java.util.concurrent
包中的Executors
工厂类来创建线程池。
我们可以创建多种类型的 ThreadPoolExecutor
:
FixedThreadPool
:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。SingleThreadExecutor
: 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。CachedThreadPool
: 该方法返回一个可根据实际情况调整线程数量的线程池。初始大小为 0。当有新任务提交时,如果当前线程池中没有线程可用,它会创建一个新的线程来处理该任务。如果在一段时间内(默认为 60 秒)没有新任务提交,核心线程会超时并被销毁,从而缩小线程池的大小。ScheduledThreadPool
:该方法返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。对应 Executors
工厂类中的方法如图所示:
详见 阿里巴巴Java开发手册 中的 (七)并发处理
FixedThreadPool
和 SingleThreadExecutor
:使用的是无界的 LinkedBlockingQueue
,任务队列最大长度为 Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。
CachedThreadPool
:使用的是同步队列 SynchronousQueue
, 允许创建的线程数量为 Integer.MAX_VALUE
,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。
ScheduledThreadPool
和 SingleThreadScheduledExecutor
: 使用的无界的延迟阻塞队列DelayedWorkQueue
,任务队列最大长度为 Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。
可以使用ThreadPoolExecutor
类直接创建一个自定义线程池,需要的参数如下图。
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 核心线程数 - 即使空闲也会保持存活的线程的数量
int corePoolSize = 5;
// 最大线程数 - 可以同时活跃的最大线程数量
int maximumPoolSize = 10;
// 线程没有任务执行时保持存活的时间
long keepAliveTime = 120;
// keepAliveTime的时间单位
TimeUnit unit = TimeUnit.SECONDS;
// 工作队列 - 存放待处理任务的队列
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(50);
// 线程工厂 - 用于创建新线程的工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略 - 当线程池和队列都满时如何处理新提交的任务
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 使用线程池执行任务
// 示例任务,实际应用中应替换为实际的Runnable任务
threadPool.execute(() -> {
System.out.println("任务执行了");
});
// 在应用程序结束时关闭线程池
threadPool.shutdown();
}
}
参考上面的图片可以知道,创建线程池需要7个参数:corePoolSize
,maximumPoolSize
,keepAliveTime
,unit
,workQueue
,threadFactory
,handler
。
corePoolSize
:核心线程池的数量,这个参数指定了线程池中的核心线程数。核心线程会一直存活,即使它们没有任务要执行。设置核心线程数可以预防线程的频繁创建和销毁,从而提高效率。maximumPoolSize
:最大线程池数量,这是线程池允许创建的最大线程数。当工作队列满了之后,线程池会创建新线程,直到线程数达到maximumPoolSize
。如果达到这个限制,新来的任务将会被拒绝处理。keepAliveTime
:空闲线程存活时间,当线程数超过corePoolSize
时,这个参数就会起作用。如果当前线程池中的线程数超过corePoolSize
,而且一个线程空闲的时间超过了keepAliveTime
,那么这个线程将会被终止,直到线程池中的线程数回落到corePoolSize
。workQueue
:工作队列,这个参数是一个BlockingQueue
,用于存放等待被执行的任务。这个队列只会处理Runnable
任务。新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。threadFactory
:线程工厂,一个ThreadFactory
,用于设置创建线程的方式。可以通过线程工厂为线程池中的线程设置名字,定义优先级等。handler
:拒绝策略,当线程池已经达到最大线程数,且工作队列也已满,新提交的任务就会通过这个拒绝执行处理器来处理。AbortPolicy:这是默认的拒绝策略。当线程池无法接受新任务时,会抛出RejectedExecutionException
异常。这意味着新任务会被立即拒绝,不会加入到任务队列中,也不会执行。通常情况下都是使用这种拒绝策略。
import java.util.concurrent.*;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 核心线程数 - 即使空闲也会保持存活的线程的数量
int corePoolSize = 2;
// 最大线程数 - 可以同时活跃的最大线程数量
int maximumPoolSize = 4;
// 线程没有任务执行时保持存活的时间
long keepAliveTime = 120;
// keepAliveTime的时间单位
TimeUnit unit = TimeUnit.SECONDS;
// 工作队列 - 存放待处理任务的队列
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);
// 线程工厂 - 用于创建新线程的工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略 - 当线程池和队列都满时如何处理新提交的任务
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 使用线程池执行任务
// 示例任务,实际应用中应替换为实际的Runnable任务
// 执行的线程数
for (int i = 0; i < 7; i++) {
try {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + ":" + "任务执行了");
});
} catch (RejectedExecutionException e) {
System.out.println("任务被拒绝: " + e.getMessage());
}
}
// 在应用程序结束时关闭线程池
threadPool.shutdown();
}
}
结果为
pool-1-thread-1:任务执行了
pool-1-thread-4:任务执行了
任务被拒绝: Task CustomThreadPoolExample$$Lambda$1/1096979270@7cca494b rejected from java.util.concurrent.ThreadPoolExecutor@7ba4f24f[Running, pool size = 4, active threads = 4, queued tasks = 2, completed tasks = 0]
pool-1-thread-3:任务执行了
pool-1-thread-2:任务执行了
pool-1-thread-4:任务执行了
pool-1-thread-1:任务执行了
由于corePoolSize
和maximumPoolSize
分别为2和4,工作队列大小为2,所以当提交第7个任务时,线程池和队列都已满,AbortPolicy
将被触发,随后的任务将被拒绝,并抛出RejectedExecutionException
异常。
CallerRunsPolicy:这种策略不会抛弃任务,也不会抛出异常。相反,它会让提交任务的线程自己执行该任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。适用于任务提交者能够承受任务执行的压力,但希望有一种缓冲机制的情况。
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 核心线程数 - 即使空闲也会保持存活的线程的数量
int corePoolSize = 2;
// 最大线程数 - 可以同时活跃的最大线程数量
int maximumPoolSize = 4;
// 线程没有任务执行时保持存活的时间
long keepAliveTime = 120;
// keepAliveTime的时间单位
TimeUnit unit = TimeUnit.SECONDS;
// 工作队列 - 存放待处理任务的队列
BlockingQueue workQueue = new LinkedBlockingQueue<>(2);
// 线程工厂 - 用于创建新线程的工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略 - 当线程池和队列都满时如何处理新提交的任务
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 使用线程池执行任务
// 示例任务,实际应用中应替换为实际的Runnable任务
// 执行的线程数
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + ":" + "任务执行了");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
// 在应用程序结束时关闭线程池
threadPool.shutdown();
}
}
结果为
pool-1-thread-2:任务执行了
pool-1-thread-4:任务执行了
pool-1-thread-1:任务执行了
pool-1-thread-3:任务执行了
main:任务执行了
main:任务执行了
pool-1-thread-2:任务执行了
pool-1-thread-3:任务执行了
pool-1-thread-2:任务执行了
pool-1-thread-4:任务执行了
当主线程开始提交任务时,首先会创建2个核心线程来执行前两个任务。随后的两个任务会被放入队列中。当主线程尝试提交第5个任务时,由于核心线程正在忙碌,队列也已满,线程池会尝试创建一个新的线程来处理这个任务,直到达到maximumPoolSize
。一旦线程池中的线程数达到4(maximumPoolSize
),并且队列也满了,主线程会执行提交的任务。
DiscardPolicy:这个策略在任务队列已满时,会丢弃新的任务而且不会抛出异常。新任务提交后会被默默地丢弃,不会有任何提示或执行。这个策略一般用于日志记录、统计等不是非常关键的任务。
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 核心线程数 - 即使空闲也会保持存活的线程的数量
int corePoolSize = 2;
// 最大线程数 - 可以同时活跃的最大线程数量
int maximumPoolSize = 4;
// 线程没有任务执行时保持存活的时间
long keepAliveTime = 120;
// keepAliveTime的时间单位
TimeUnit unit = TimeUnit.SECONDS;
// 工作队列 - 存放待处理任务的队列
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);
// 线程工厂 - 用于创建新线程的工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略 - 当线程池和队列都满时如何处理新提交的任务
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 使用线程池执行任务
// 示例任务,实际应用中应替换为实际的Runnable任务
// 执行的线程数
for (int i = 0; i < 7; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + ":" + "任务执行了");
});
}
// 在应用程序结束时关闭线程池
threadPool.shutdown();
}
}
结果为
pool-1-thread-1:任务 1 开始执行
pool-1-thread-4:任务 6 开始执行
pool-1-thread-3:任务 5 开始执行
pool-1-thread-2:任务 2 开始执行
pool-1-thread-4:任务 7 开始执行
pool-1-thread-1:任务 4 开始执行
由于corePoolSize
和maximumPoolSize
分别为2和4,工作队列大小为2,所以当提交第7个任务时,线程池和队列都已满,DiscardPolicy
将被触发,随后的任务将被丢弃。
DiscardOldestPolicy:这个策略也会丢弃新的任务,但它会先尝试将任务队列中最早的任务删除,然后再尝试提交新任务。如果任务队列已满,且线程池中的线程都在工作,可能会导致一些任务被丢弃。这个策略对于一些实时性要求较高的场景比较合适。
import java.util.concurrent.*;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 核心线程数 - 即使空闲也会保持存活的线程的数量
int corePoolSize = 2;
// 最大线程数 - 可以同时活跃的最大线程数量
int maximumPoolSize = 4;
// 线程没有任务执行时保持存活的时间
long keepAliveTime = 120;
// keepAliveTime的时间单位
TimeUnit unit = TimeUnit.SECONDS;
// 工作队列 - 存放待处理任务的队列
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);
// 线程工厂 - 用于创建新线程的工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略 - 当线程池和队列都满时如何处理新提交的任务
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 使用线程池执行任务
// 示例任务,实际应用中应替换为实际的Runnable任务
// 执行的线程数
for (int i = 0; i < 8; i++) {
int taskId = i;
System.out.println(Thread.currentThread().getName() + ":" + "任务 " + taskId + " 被提交");
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + ":" + "任务 " + taskId + " 开始执行");
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + ":" + "任务 " + taskId + " 执行结束");
});
}
// 在应用程序结束时关闭线程池
threadPool.shutdown();
}
}
为了能观察到哪些任务提交了但是未被执行,加入了taskId变量来观察任务的执行情况。
结果为
main:任务 1 被提交
main:任务 2 被提交
main:任务 3 被提交
main:任务 4 被提交
pool-1-thread-1:任务 1 开始执行
pool-1-thread-2:任务 2 开始执行
main:任务 5 被提交
main:任务 6 被提交
main:任务 7 被提交
pool-1-thread-3:任务 5 开始执行
main:任务 8 被提交
pool-1-thread-4:任务 6 开始执行
pool-1-thread-1:任务 1 执行结束
pool-1-thread-2:任务 2 执行结束
pool-1-thread-3:任务 5 执行结束
pool-1-thread-4:任务 6 执行结束
pool-1-thread-3:任务 8 开始执行
pool-1-thread-1:任务 7 开始执行
pool-1-thread-3:任务 8 执行结束
pool-1-thread-1:任务 7 执行结束
由于corePoolSize
和maximumPoolSize
分别为2和4,工作队列大小为2,所以当提交第7个任务时,线程池和队列都已满,任务1,2已经开始执行,而任务3尚未执行,所以任务3等待时间最长触发DiscardOldestPolicy
策略被删除,同理任务8提交时,任务4尚未执行等待时间最长触发DiscardOldestPolicy
策略被删除。
阿里巴巴编码规范对于线程池的使用有一些规范和建议,以下是一些主要的指导原则:
线程池基本规范:
ThreadPoolExecutor
创建线程池:尽量使用ThreadPoolExecutor
类创建线程池,以便灵活地配置线程池参数。线程池参数配置:
LinkedBlockingQueue
)可能导致队列无限增长,最终耗尽系统资源。建议使用有界队列来限制队列的长度。拒绝策略选择:
CallerRunsPolicy
,避免直接抛出异常或丢弃任务。避免线程池滥用:
任务提交方式:
execute
和submit
合理:execute
适用于不需要获取执行结果的场景,而submit
适用于需要获取执行结果的场景。处理异常:
及时处理任务中的异常:对于submit
方法提交的任务,需要通过Future.get()
来检查任务执行结果,包括异常。如果不及时处理异常,可能导致任务失败而无法及时发现问题。
谨慎使用线程池:线程池不是万能的,不适合所有场景。在某些特定的业务场景中,可能需要考虑使用其他并发控制手段,如信号量、CountDownLatch 等。
任务提交方式:
execute
和submit
合理:execute
适用于不需要获取执行结果的场景,而submit
适用于需要获取执行结果的场景。处理异常:
submit
方法提交的任务,需要通过Future.get()
来检查任务执行结果,包括异常。如果不及时处理异常,可能导致任务失败而无法及时发现问题。这些规范和建议有助于确保线程池的稳定运行,提高系统的性能和可维护性。在具体应用时,还需根据具体业务场景和需求进行适度调整。