线程池,及7大参数,4大拒绝策略详解
1. 前言
1.1 什么是线程池?
线程池是一种利用池化技术思想来实现的线程管理技术,主要是为了复用线程、便利地管理线程和任务、并将线程的创建和任务的执行解耦开来。我们可以创建线程池来复用已经创建的线程来降低频繁创建和销毁线程所带来的资源消耗。在JAVA中主要是使用ThreadPoolExecutor类来创建线程池,并且JDK中也提供了Executors工厂类来创建线程池(不推荐使用)。
线程池的优点:
降低资源消耗,复用已创建的线程来降低创建和销毁线程的消耗。
提高响应速度,任务到达时,可以不需要等待线程的创建立即执行。
提高线程的可管理性,使用线程池能够统一的分配、调优和监控。
1.2 为什么使用线程池?
没有线程池时
从上面可以看出之前显示的创建线程的一些缺点:
1)不受控制风险,对于每个创建的线程没有统一管理的地方,每个线程创建后我们不知道线程的去向。
2)每执行一个任务都需要创建新的线程来执行,创建线程对系统来说开销很高
而若是用线程池来管理线程
执行相同任务时可以复用线程并不用频繁创建和销毁。
线程池的好处:
方便管理
提高响应的速度
线程复用、控制最大并发数、管理线程
减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。
如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
2.JAVA线程池概述
Java中线程池的核心实现类是ThreadPoolExecutor,可以通过该类地构造方法来构造一个线程池,我们先来看下ThreadPoolExecutor的整个继承体系
Executor接口:
java.util.concurrent.Executor
接口提供了一种将任务的提交与任务的执行分离的机制。它包含一个单一的方法execute(Runnable command)
,用于执行传递的任务。
ExecutorService接口:
java.util.concurrent.ExecutorService
接口扩展了Executor
接口,提供了更丰富的功能来管理线程池。
AbstractExecutorService类:
AbstractExecutorService
类是ExecutorService
接口的一个抽象实现,提供了一些默认实现和辅助方法,使得实现自定义的线程池变得更加容易。该类实现了部分ExecutorService
接口中的方法,留下了一些抽象方法需要具体的子类来实现。
ThreadPoolExecutor类:
java.util.concurrent.ThreadPoolExecutor
是ExecutorService
接口的一个实现,它提供了一个灵活且可扩展的线程池实现。- 通过
ThreadPoolExecutor
的构造方法可以创建一个线程池,配置核心线程数、最大线程数、线程空闲时间、工作队列等参数,以满足不同场景的需求。
2.1七大参数
ThreadPoolExecutor类提供了七个参数,这些参数用于配置线程池的行为。
- corePoolSize(核心线程数):
- 线程池的基本大小,即在没有任务需要执行时,线程池的大小是多少。
- 如果调用了
prestartAllCoreThreads()
方法,线程池会在启动时提前创建并启动所有的核心线程。
- maximumPoolSize(最大线程数):
- 线程池允许创建的最大线程数。如果队列满了,并且活动线程数小于最大线程数,线程池会创建新的线程来执行任务。
- 如果使用了无界队列(例如LinkedBlockingQueue),则该参数就不起作用。
- keepAliveTime(空闲线程存活时间):
- 当线程池中的线程数量大于核心线程数时,多余的空闲线程在终止之前等待新任务的最长时间。超过这个时间就会被回收。
- 如果设置了allowCoreThreadTimeOut为true,则核心线程也会超时终止。
- unit(空闲线程存活时间的单位):
keepAliveTime
参数的时间单位,通常是 TimeUnit.SECONDS 或 TimeUnit.MILLISECONDS。
- workQueue(阻塞队列):
- 用于保存等待执行的任务的阻塞队列。可以选择不同类型的队列,例如 LinkedBlockingQueue、ArrayBlockingQueue 等。
- 这是线程池的关键参数之一,不同的队列类型会影响线程池的行为。
- threadFactory(线程工厂):
- 用于创建新线程的工厂。可以通过实现 ThreadFactory 接口来自定义线程的创建过程,例如为线程指定名称、优先级等。
- handler(拒绝策略):
- 当工作队列满并且线程池中的线程数达到最大线程数时,用于处理新提交的任务的策略。
简单使用线程
public class Main {
public static void main(String[] args) {
int corePoolSize = 4; //核心线程数
int maximumPoolSize = 8; //最大线程数
long keepAliveTime = 10000; //空闲线程存活时间
TimeUnit unit = TimeUnit.MILLISECONDS; //空闲线程存活时间的单位
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(8); //阻塞队列
ThreadFactory threadFactory = new ThreadFactory() { //线程工厂
@Override
public Thread newThread(Runnable r) {
System.out.println("创建线程:" + r);
return new Thread(r);
}
};
RejectedExecutionHandler handler = null;//拒绝策略
ThreadPoolExecutor myThreadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < 16; i++) {
myThreadPool.execute(() -> {
// 执行的线程
for (int j = 0; j < 10; j++) {
System.out.println(Thread.currentThread() + ":执行" + j + "次");
}
});
}
myThreadPool.shutdown();
}
}
2.2四大拒绝策略
Java中的线程池在任务提交过程中,如果线程池已满且无法继续创建新线程,就会触发拒绝策略。Java提供了四种预定义的拒绝策略,它们分别是:
-
AbortPolicy(中止策略):
- 默认的拒绝策略,会直接抛出
RejectedExecutionException
异常,阻止系统正常运行。
- 默认的拒绝策略,会直接抛出
-
CallerRunsPolicy(调用者运行策略):
- 将任务回退给调用线程来执行。在这个策略中,任务提交者会自己去执行该任务,从而降低新任务的提交速度,以适应系统的处理能力。
-
DiscardPolicy(丢弃策略):
- 直接丢弃新的任务,没有任何处理。如果系统对任务丢失不敏感,可以使用这个策略。
-
DiscardOldestPolicy(丢弃最老策略):
- 丢弃队列中最老的任务,然后尝试重新提交当前任务。这样可以腾出队列空间来接收新的任务,但可能会丢失一些等待执行的任务。
这些拒绝策略提供了不同的处理方式,可以根据实际场景和需求选择合适的策略。通过 setRejectedExecutionHandler
方法,可以在创建 ThreadPoolExecutor
实例时指定拒绝策略。
2.3测试四大拒绝策略
-
AbortPolicy(中止策略):
public class Main { public static void main(String[] args) { // 创建线程池 ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 2, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); // 创建4个任务 for(int i = 0; i < 4; i++) { pool.execute(() -> System.out.println(Thread.currentThread().getName() + " ... ")); } // 关闭线程池 pool.shutdown(); } }
执行结果:
分析:
最大的线程数是2,阻塞队列中可以存一个,最多就可接收3个任务,当有4个任务时,就会执行拒绝策略,此拒绝策略为丢弃任务,抛出异常
-
CallerRunsPolicy(调用者运行策略):
public class Main { public static void main(String[] args) { // 创建线程池 ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 2, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); // 创建4个任务 for(int i = 0; i < 4; i++) { pool.execute(() -> System.out.println(Thread.currentThread().getName() + " ... ")); } // 关闭线程池 pool.shutdown(); } }
执行结果:
分析:
最大的线程数是2,阻塞队列中可以存一个,最多就可接收3个任务,当有4个任务时,就会执行拒绝策略,因为是主线程创建的线程,所以当任务没有线程执行的时候,会由创建此线程的线程来执行,也就是由主线程来执行,主线程名为main
-
DiscardPolicy(丢弃策略):
public class Main { public static void main(String[] args) { // 创建线程池 ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 2, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy()); // 创建4个任务 for(int i = 0; i < 4; i++) { pool.execute(() -> System.out.println(Thread.currentThread().getName() + " ... ")); } // 关闭线程池 pool.shutdown(); } }
执行结果:
分析:
最大的线程数是2,阻塞队列中可以存一个,最多就可接收3个任务,当有4个任务时,就会执行拒绝策略,丢弃新产生的任务
-
DiscardOldestPolicy(丢弃最老策略):
public class Main { public static void main(String[] args) { // 创建线程池 ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 2, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy()); // 创建4个任务 for(int i = 0; i < 4; i++) { pool.execute(() -> System.out.println(Thread.currentThread().getName() + " ... ")); } // 关闭线程池 pool.shutdown(); } }
执行结果:
分析:
有2线程执行,第3个任务超出了最大线程数,所以进入阻塞队列,第4个任务发现了阻塞队列满了,此时最大线程数已经达到最大值了,而阻塞队列也满了,所以执行拒绝策略:丢弃阻塞队列中老任务(也就是第3个任务),将新的任务(第4个任务)添加进去,最后等前面任务执行完,释放线程后,执行了阻塞队列中的任务,也就是任务4被执行了,所以共执行了3个任务,丢弃了一个(任务3)
总结
阿里巴巴编码规范对于线程池的使用有一些规范和建议,以下是一些主要的指导原则:
- 线程池基本规范:
- 推荐使用线程池的方式创建线程:使用线程池可以减少线程的创建和销毁开销,提高系统的性能。
- 使用
ThreadPoolExecutor
创建线程池:尽量使用ThreadPoolExecutor
类创建线程池,以便灵活地配置线程池参数。
- 线程池参数配置:
- 避免使用无界队列:无界队列(如
LinkedBlockingQueue
)可能导致队列无限增长,最终耗尽系统资源。建议使用有界队列来限制队列的长度。 - 合理配置线程池大小:根据业务场景和系统资源,合理配置核心线程数、最大线程数、存活时间等参数,以优化线程池的性能。
- 避免使用固定大小的线程池:固定大小的线程池可能在高并发时无法处理大量的请求,建议根据实际需求使用可伸缩的线程池。
- 避免使用无界队列:无界队列(如
- 拒绝策略选择:
- 慎重选择拒绝策略:根据实际业务场景,选择合适的拒绝策略。通常情况下,建议使用
CallerRunsPolicy
,避免直接抛出异常或丢弃任务。 - 定制拒绝策略:如果默认的拒绝策略无法满足需求,可以实现自定义的拒绝策略。
- 慎重选择拒绝策略:根据实际业务场景,选择合适的拒绝策略。通常情况下,建议使用
- 避免线程池滥用:
- 谨慎使用线程池:线程池不是万能的,不适合所有场景。在某些特定的业务场景中,可能需要考虑使用其他并发控制手段,如信号量、CountDownLatch 等。
- 任务提交方式:
- 使用
execute
和submit
合理:execute
适用于不需要获取执行结果的场景,而submit
适用于需要获取执行结果的场景。
- 使用
- 处理异常:
- 及时处理任务中的异常:对于
submit
方法提交的任务,需要通过Future.get()
来检查任务执行结果,包括异常。如果不及时处理异常,可能导致任务失败而无法及时发现问题。
- 及时处理任务中的异常:对于
- 谨慎使用线程池:线程池不是万能的,不适合所有场景。在某些特定的业务场景中,可能需要考虑使用其他并发控制手段,如信号量、CountDownLatch 等。
- 任务提交方式:
- 使用
execute
和submit
合理:execute
适用于不需要获取执行结果的场景,而submit
适用于需要获取执行结果的场景。
- 使用
- 处理异常:
- 及时处理任务中的异常:对于
submit
方法提交的任务,需要通过Future.get()
来检查任务执行结果,包括异常。如果不及时处理异常,可能导致任务失败而无法及时发现问题。
- 及时处理任务中的异常:对于
这些规范和建议有助于确保线程池的稳定运行,提高系统的性能和可维护性。在具体应用时,还需根据具体业务场景和需求进行适度调整。