目录
线程池选择
使用 Executors 工具类
Executors 是一个工具类,提供了多种静态方法来创建不同类型的线程池。以下是几种常用的方法:
固定大小的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
- 特点:线程池中的线程数量是固定的,适合处理大量短时间的任务。
单线程线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
- 特点:只有一个线程,所有任务都在这个线程中顺序执行,适合需要保证任务顺序执行的场景。
缓存线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- 特点:线程池中的线程数量不是固定的,可以根据需要创建新的线程。空闲线程会被回收,适合执行大量的短期异步任务。
定时任务线程池
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
- 特点:支持定时任务和周期性任务的执行。可以调度命令在给定的延迟后只执行一次,或者定期执行。
使用 ThreadPoolExecutor 类
ThreadPoolExecutor 提供了更灵活的线程池配置选项,适用于需要更精细控制的场景。
import java.util.concurrent.*;
public class CustomThreadPool {
public static void main(String[] args) {
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 10L;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 使用线程池执行任务
for (int i = 0; i < 20; i++) {
final int taskId = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("Task ID: " + taskId + " is running by " + Thread.currentThread().getName());
}
});
}
// 关闭线程池
threadPool.shutdown();
}
}
参数说明:
- corePoolSize:核心线程数,线程池保持的核心线程数。
- maximumPoolSize:最大线程数,线程池允许的最大线程数。
- keepAliveTime:线程空闲时间,当线程数超过核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。
- unit:时间单位,用作于线程空闲时间的单位。
- workQueue:任务队列,用于存放等待执行的任务。
- threadFactory:线程工厂,用于创建新线程。
- handler:拒绝策略,当线程池无法处理新任务时的处理策略。
使用 ForkJoinPool
ForkJoinPool 是一个特殊的线程池,适用于可以分解为子任务的大型任务,特别适合处理计算密集型任务。
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class ForkJoinExample extends RecursiveTask<Integer> {
private final int threshold = 1000;
private final int start;
private final int end;
public ForkJoinExample(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= threshold) {
return computeDirectly();
} else {
int mid = (start + end) / 2;
ForkJoinExample leftTask = new ForkJoinExample(start, mid);
ForkJoinExample rightTask = new ForkJoinExample(mid, end);
leftTask.fork();
rightTask.fork();
return leftTask.join() + rightTask.join();
}
}
private int computeDirectly() {
int sum = 0;
for (int i = start; i < end; i++) {
sum += i;
}
return sum;
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinExample task = new ForkJoinExample(1, 1000000);
int result = forkJoinPool.invoke(task);
System.out.println("Result: " + result);
}
}
总结
- Executors 工具类:适合快速创建简单的线程池。
- ThreadPoolExecutor 类:适合需要更灵活配置的线程池。
- ForkJoinPool:适合处理可以分解为子任务的大型任务。
ThreadPoolExecutor参数的设计
理解 ThreadPoolExecutor 中的核心线程数、最大线程数、线程空闲时间、任务队列、线程工厂和拒绝策略的作用,可以帮助你更好地设计和优化线程池,从而提升系统的性能和稳定性。下面详细解释这些参数的作用及其设计目的,并探讨它们在实际业务中的好处。
1. 核心线程数(corePoolSize)
作用:
- 核心线程数是线程池中始终保持活动状态的线程数,即使这些线程处于空闲状态,也不会被回收。
- 当新任务提交到线程池时,如果当前活动的线程数少于核心线程数,线程池会创建新的线程来处理任务。
设计目的:
- 快速响应:确保线程池始终有足够的线程来快速响应新任务,提高系统的响应速度。
- 资源控制:通过限制核心线程数,可以避免在系统启动初期创建过多线程,从而节省资源。
实际业务中的好处:
- 提高系统响应速度:确保系统在正常负载下能够快速响应新任务。
- 资源利用率:合理设置核心线程数可以平衡系统资源的使用,避免资源浪费。
2. 最大线程数(maximumPoolSize)
作用:
- 最大线程数是线程池允许创建的最大线程数量。
- 当核心线程数已满,任务队列也已满时,如果当前活动的线程数少于最大线程数,线程池会创建新的线程来处理任务。
设计目的:
- 防止资源耗尽:通过设置最大线程数,可以防止系统因创建过多线程而导致资源耗尽。
- 任务处理能力:在高负载情况下,通过增加线程数来提高任务处理能力。
实际业务中的好处:
- 防止系统崩溃:避免因线程数过多而导致系统崩溃。
- 弹性扩展:在高负载情况下,系统可以自动扩展线程数,提高任务处理能力。
3. 线程空闲时间(keepAliveTime)
作用:
- 当线程池中的线程数超过核心线程数时,多余的线程在空闲时间超过 keepAliveTime 后会被回收。
- 如果设置了 allowCoreThreadTimeOut 为 true,核心线程也会在空闲时间超过 keepAliveTime 后被回收。
设计目的:
- 资源释放:及时释放不再使用的线程资源,避免资源浪费。
- 动态调整:根据系统负载动态调整线程池的大小,提高资源利用率。
实际业务中的好处: - 节省资源:在低负载情况下,及时回收空闲线程,节省系统资源。
- 灵活性:可以根据系统负载动态调整线程池的大小,提高系统的灵活性和适应性。
4. 任务队列(workQueue)
作用:
- 任务队列用于存储等待执行的任务。
- 当线程池中的线程数达到核心线程数时,新提交的任务会被放入任务队列中等待执行。
- 当任务队列已满且线程数未达到最大线程数时,线程池会创建新的线程来处理任务。
设计目的:
- 任务缓冲:通过任务队列缓冲新提交的任务,平滑处理任务的突发流量。
- 任务管理:提供不同的队列类型(如 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等),以满足不同的任务管理需求。
实际业务中的好处:
- 平滑处理任务:通过任务队列平滑处理任务的突发流量,避免系统因任务激增而崩溃。
- 灵活的任务管理:根据不同场景选择合适的任务队列类型,提高任务管理的灵活性和效率。
5. 线程工厂(threadFactory)
作用:
- 线程工厂用于创建新线程,可以自定义线程的属性,如线程名称、优先级等。
- 通过自定义线程工厂,可以在创建线程时进行一些初始化操作,如设置线程名称、优先级等。
设计目的:
- 线程管理:通过自定义线程工厂,可以更好地管理和跟踪线程,便于调试和维护。
- 性能优化:通过设置合理的线程属性,可以优化系统的性能。
实际业务中的好处:
- 便于调试和维护:通过设置线程名称,可以在日志中更容易地识别和追踪线程,便于调试和维护。
- 性能优化:通过设置合理的线程优先级,可以优化系统的性能,提高任务处理效率。
6. 拒绝策略(RejectedExecutionHandler)
作用:
- 当线程池和任务队列都已满时,新提交的任务将触发拒绝策略。
- 常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用者线程执行任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最旧的任务)等。
设计目的:
- 任务管理:通过不同的拒绝策略,可以灵活地处理无法处理的新任务,避免任务堆积。
- 系统保护:防止系统因任务堆积而导致资源耗尽或崩溃。
实际业务中的好处:
- 任务管理:根据业务需求选择合适的拒绝策略,确保系统在高负载下仍然能够稳定运行。
- 系统保护:通过合理的拒绝策略,可以保护系统免受高负载的影响,避免系统崩溃。
总结
通过合理设置 ThreadPoolExecutor 中的核心线程数、最大线程数、线程空闲时间、任务队列、线程工厂和拒绝策略,可以显著提升系统的性能和稳定性。这些参数的设计目的是为了平衡系统资源的使用,提高任务处理能力,防止资源耗尽,并提供灵活的任务管理和线程管理。在实际业务中,合理配置这些参数可以更好地应对各种负载情况,确保系统的高效运行。
ThreadPoolExecutor参数如何合理设置
核心线程数设置
CPU 密集型任务
对于CPU密集型任务,核心线程数通常设置为CPU核心数。这是因为每个核心只能同时执行一个线程,过多的线程会导致频繁的上下文切换,降低性能。
计算公式
int cpuCoreCount = Runtime.getRuntime().availableProcessors();
int corePoolSize = cpuCoreCount;
I/O 密集型任务
对于I/O密集型任务,核心线程数可以设置得更大一些。因为在等待I/O操作完成期间,线程可以被释放去执行其他任务,从而提高整体吞吐量。
计算公式
int cpuCoreCount = Runtime.getRuntime().availableProcessors();
int corePoolSize = cpuCoreCount * 2; // 或者更大的值,根据实际情况调整
混合型任务
如果任务既包含CPU密集型部分也包含I/O密集型部分,可以考虑将任务拆分为不同类型,并分别使用不同大小的线程池来处理。也可以根据任务的特性和预期负载来动态调整核心线程数。
实际应用场景
Web 应用服务器
对于Web应用服务器,核心线程数通常取决于预期的并发请求量和每个请求的处理时间。可以参考以下公式:
int maxConcurrentRequests = 100; // 预期的最大并发请求数
int requestProcessingTime = 500; // 每个请求的平均处理时间(毫秒)
int requestInterval = 100; // 请求之间的平均间隔时间(毫秒)
int corePoolSize = maxConcurrentRequests * (requestProcessingTime / requestInterval);
数据处理任务
对于数据处理任务,可以根据数据处理的速度和数据生成的速度来调整核心线程数。可以参考以下公式:
int dataProcessingRate = 1000; // 每秒处理的数据量
int dataGenerationRate = 800; // 每秒生成的数据量
int corePoolSize = (dataProcessingRate / dataGenerationRate) *