为什么需要自定义线程池
- 资源竞争:ForkJoinPool.commonPool() 是一个公共线程池,多个不同类型任务可能会竞争资源,导致性能下降。例如,I/O 密集型任务和 CPU 密集型任务共用此线程池,I/O 任务长时间等待 I/O 操作完成,会阻塞 CPU 密集型任务的执行。
- 线程饥饿:如果高并发场景下某些任务执行时间长,可能会导致其他任务无法及时获取线程资源,出现线程饥饿现象。
- 缺乏针对性优化:不同类型任务对线程池配置需求不同,公共线程池无法满足这种多样性需求。比如大数据处理任务可能需要更多线程,而一些简单的轻量级任务可能适合较少线程的线程池。
如何自定义线程池以优化任务调度
- 创建ThreadPoolExecutor:可以使用
ThreadPoolExecutor
类来自定义线程池。例如:
import java.util.concurrent.*;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 核心线程数
int corePoolSize = 5;
// 最大线程数
int maximumPoolSize = 10;
// 线程存活时间
long keepAliveTime = 10;
TimeUnit unit = TimeUnit.SECONDS;
// 任务队列
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(20);
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue);
CompletableFuture.supplyAsync(() -> {
// 任务逻辑
return "Result";
}, executor);
}
}
- 使用CompletableFuture时传入自定义线程池:在
supplyAsync
方法中,第二个参数传入自定义的 ThreadPoolExecutor
对象,如上述代码中所示。
自定义线程池时需要考虑的关键因素
- 核心线程数:核心线程数决定了线程池中常驻的线程数量。应根据任务类型和硬件资源确定,对于 CPU 密集型任务,核心线程数一般设置为 CPU 核心数;对于 I/O 密集型任务,核心线程数可适当增加,因为 I/O 操作等待时线程可以处理其他任务。
- 最大线程数:最大线程数限制了线程池能创建的最多线程数量。设置过大可能导致系统资源耗尽,过小则无法充分利用系统资源。需要综合考虑系统的负载能力和任务的并发度。
- 线程存活时间:非核心线程在没有任务执行时,等待新任务的存活时间。设置过长会浪费资源,过短则频繁创建和销毁线程带来额外开销。
- 任务队列:任务队列用于存放等待执行的任务。不同类型的队列有不同特性,如
ArrayBlockingQueue
是有界队列,LinkedBlockingQueue
可以是无界队列(也可设置有界)。选择合适的队列类型可以避免任务堆积导致内存溢出,或因队列过小导致任务无法及时提交。
- 拒绝策略:当任务队列已满且线程池达到最大线程数时,新任务需要有拒绝策略。常见策略包括
AbortPolicy
(抛出异常)、CallerRunsPolicy
(在调用者线程执行任务)、DiscardPolicy
(丢弃任务)、DiscardOldestPolicy
(丢弃队列中最老的任务)。应根据业务需求选择合适的拒绝策略。