自定义线程池
线程池(ThreadPool)在Java中是通过
Executor
框架实现的,它允许你以池化的方式管理线程,复用线程并控制最大并发数,从而提高资源的利用率和系统的稳定性。ThreadPoolExecutor
是Executor
接口的一个实现,它有7个核心参数和4种拒绝策略。
7大参数
- corePoolSize:
- 核心线程数,即使它们是空闲的,线程池也会保持存活的线程数量。
- maximumPoolSize:
- 线程池能够容纳同时执行的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。
- keepAliveTime:
- 当线程数大于核心线程数时,这是多余空闲线程在终止前等待新任务的最长时间。
- unit:
keepAliveTime
的时间单位。
- workQueue:
- 用于保存等待执行的任务的阻塞队列。常用的队列有如下几种:
ArrayBlockingQueue
:基于数组结构的有界阻塞队列,按 FIFO 排序任务。LinkedBlockingQueue
:基于链表结构的阻塞队列,吞吐量通常要高于ArrayBlockingQueue
。SynchronousQueue
:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,反之亦然。PriorityBlockingQueue
:具有优先级的无界阻塞队列。
- 用于保存等待执行的任务的阻塞队列。常用的队列有如下几种:
- threadFactory:
- 用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
- handler:
- 拒绝策略,当任务太多来不及处理,如何拒绝任务。
4大拒绝策略
- ThreadPoolExecutor.AbortPolicy:
- 抛出
RejectedExecutionException
异常来拒绝新任务的处理。
- 抛出
- ThreadPoolExecutor.CallerRunsPolicy:
- 调用任务的
run()
方法绕过线程池直接执行。
- 调用任务的
- ThreadPoolExecutor.DiscardPolicy:
- 不处理新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy:
- 丢弃队列中最老的一个任务,并尝试再次提交当前任务。
实例代码
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS, // unit
new LinkedBlockingQueue<>(100), // workQueue
Executors.defaultThreadFactory(), // threadFactory
new ThreadPoolExecutor.AbortPolicy() // handler
);
// 提交任务
executor.execute(() -> {
// 任务代码
});
// 关闭线程池
executor.shutdown();
}
}
核心线程数详解
线程池的线程核心数(core pool size)是指线程池中保持活跃的线程数量,即使它们处于空闲状态。这个数值是在创建线程池时设置的,并且可以根据具体的应用需求进行调整。线程池的核心数与CPU的内核数(core count)有一定的关系,但它们并不是直接对应的。
CPU的内核数是指物理CPU中的核心数量。一个CPU核心可以独立执行计算任务,而多核心CPU可以同时执行多个计算任务,从而提高并行处理能力。在设计并发程序时,理解CPU的内核数对于优化线程池的大小是很重要的。
以下是线程池线程核心数与CPU内核数关系的一些考虑因素:
- CPU密集型任务:
如果你的应用程序主要执行CPU密集型任务(如复杂的计算),那么线程池的大小通常设置为CPU内核数的数量或稍微多一点。这是因为CPU密集型任务会持续使用CPU资源,而增加更多的线程并不会提高性能,因为CPU核心已经饱和。 - IO密集型任务:
如果你的应用程序主要执行IO密集型任务(如文件操作、网络通信等),那么线程池的大小可以设置得比CPU内核数多得多。这是因为IO密集型任务经常会因为等待IO操作而阻塞,这时CPU核心不会被充分利用。增加更多的线程可以确保CPU在等待IO时仍然有任务可以执行,从而提高程序的吞吐量。 - 混合型任务:
对于同时包含CPU密集型和IO密集型任务的应用程序,线程池的大小可能需要根据任务的具体特性和比例来调整。 - 上下文切换开销:
创建过多的线程可能会导致频繁的上下文切换,这会增加额外的开销,从而降低程序的整体性能。因此,即使是IO密集型任务,也应避免设置过高的线程池大小。
在实践中,确定线程池的最佳大小通常需要基于应用程序的性能测试和调优。有时候,开发者会使用经验公式来估算线程池的大小,例如:
线程池大小 = CPU内核数 * (1 + 平均等待时间 / 平均工作时间)
这个公式考虑了任务的等待时间(如IO操作)和实际工作时间(CPU计算时间)的比例。然而,这只是一个起点,最终的线程池大小应该根据实际的应用程序负载和性能指标来确定。
总结
在实际应用中,应该根据任务的性质和系统资源合理配置线程池的参数,以避免资源浪费或过载。例如,CPU 密集型任务应该配置较小的线程池,通常接近CPU核心数,而IO密集型任务由于线程并不是一直在执行任务,可以配置更多的线程。