线程池
线程池就是一种池化技术,用于预先创建并管理一组线程,避免频繁创建和销毁线程的开销,提高性能和响应速度。
他的几个关键配置包括:核心线程、最大线程数、空闲存活时间、工作队列、拒绝策略
五大线程池、七个参数、四个拒绝策略
线程池相关参数解释
corePoolSize
:核心线程数,即线程池中始终保持的线程数量。maximumPoolSize
:最大线程数,即线程池中允许的最大线程数量。keepAliveTime
:线程空闲时间,超过这个时间的非核心线程会被销毁。workQueue
:任务队列,存放待执行的任务。threadFactory
:线程工厂,用于创建新线程。rejectedExecutionHandler
:任务拒绝处理器,当任务无法执行时的处理策略。
工作队列类型
- SynchronousQueue:不存储任务,直接将任务提交给线程。
- LinkedBlockingQueue:链表结构的阻塞队列,大小无限。
- ArrayBlockingQueue:数组结构的有界阻塞队列。
- PriorityBlockingQueue:带优先级的无界阻塞队列。

主要工作原理如下:
- 默认情况下线程不会预创建,任务提交之后才会创建线程(不过设置 prestartAllCoreThreads 可以预创建核心线程)。
- 当核心线程满了之后不会新建线程,而是把任务堆积到工作队列中。
- 如果工作队列放不下了,然后才会新增线程,直至达到最大线程数。
- 如果工作队列满了,然后也已经达到最大线程数了,这时候来任务会执行拒绝策略。
- 如果线程空闲时间超过空闲存活时间,并且当前线程数大于核心线程数的则会销毁线程,直到线程数等于核心线程数(设置 allowCoreThreadTimeOut 为 true 可以回收核心线程,默认为 false)。
注意,核心线程和非核心线程在线程池中是一样的,并没有特殊的标识区分!

为什么默认情况下不会预创建线程
csharp
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 5,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>()
);
// 默认情况:还没有线程
System.out.println("Pool size before: " + executor.getPoolSize());
// 预启动一个核心线程
executor.prestartCoreThread();
System.out.println("Pool size after prestartCoreThread: " + executor.getPoolSize());
// 预启动所有核心线程
executor.prestartAllCoreThreads();
System.out.println("Pool size after prestartAllCoreThreads: " + executor.getPoolSize());
此时线程池里 一开始是没有任何线程的。
- 第一次提交任务时,才会创建一个线程来执行任务。
- 如果有空闲线程在工作队列里,新的任务会被直接放到队列中,不会马上创建新线程。
- 如果队列满了,才会继续创建新线程,直到达到
maximumPoolSize
。
也就是说:线程池是懒惰的,不浪费资源,只有来了任务才会创建线程。

prestartAllCoreThreads()

如果手动调用这个方法,那么就会预创建核心线程
Java 并发库中提供了哪些线程池实现?
Java 并发库中提供了 5 种常见的线程池实现,主要通过 Executors
工具类来创建。
一般来说。不建议使用Executors
去创建线程池 ,而是通过ThreadPoolExectors
去创建

1)FixedThreadPool:创建一个固定数量的线程池。
线程池中的线程数是固定的,空闲的线程会被复用。如果所有线程都在忙,则新任务会放入队列中等待。
适合负载稳定的场景,任务数量确定且不需要动态调整线程数。
2)CachedThreadPool:一个可以根据需要创建新线程的线程池。
线程池的线程数量没有上限,空闲线程会在 60 秒后被回收,如果有新任务且没有可用线程,会创建新线程。
适合短期大量并发任务的场景,任务执行时间短且线程数需求变化较大。
3)SingleThreadExecutor:创建一个只有单个线程的线程池。
只有一个线程处理任务,任务会按照提交顺序依次执行。
适用于需要保证任务按顺序执行的场景,或者不需要并发处理任务的情况。
4)ScheduledThreadPool:支持定时任务和周期性任务的线程池。
可以定时或以固定频率执行任务,线程池大小可以由用户指定。
适用于需要周期性任务执行的场景,如定时任务调度器。
5)WorkStealingPool:基于任务窃取算法的线程池。
线程池中的每个线程维护一个双端队列(deque),线程可以从自己的队列中取任务执行。如果线程的任务队列为空,它可以从其他线程的队列中"窃取"任务来执行,达到负载均衡的效果。
适合大量小任务并行执行,特别是递归算法或大任务分解成小任务的场景。
不同线程池的选择总结:
- FixedThreadPool 适合任务数量相对固定,且需要限制线程数的场景,避免线程过多占用系统资源。
- CachedThreadPool 更适合大量短期任务或任务数量不确定的场景,能够根据任务量动态调整线程数。
- SingleThreadExecutor 保证任务按顺序执行,适合要求严格顺序执行的场景。
- ScheduledThreadPool 是定时任务的最佳选择,能够轻松实现周期性任务调度。
- WorkStealingPool 适合处理大量的小任务,能更好地利用 CPU 资源。
Java 线程池有哪些拒绝策略?
一共提供了 4 种:
1)AbortPolicy
,当任务队列满且没有线程空闲,此时添加任务会直接抛出 RejectedExecutionException
错误 ,这也是默认的拒绝策略。适用于必须通知调用者任务未能被执行的场景。
2)CallerRunsPolicy
,当任务队列满且没有线程空闲,此时添加任务由即调用者线程执行。适用于希望通过减缓任务提交速度来稳定系统的场景。
3)DiscardOldestPolicy
,当任务队列满且没有线程空闲,会删除最早的任务,然后重新提交当前任务。适用于希望丢弃最旧的任务以保证新的重要任务能够被处理的场景。
4)DiscardPolicy
,直接丢弃当前提交的任务,不会执行任何操作,也不会抛出异常。适用于对部分任务丢弃没有影响的场景,或系统负载较高时不需要处理所有任务。
如何设置最大线程池?
线程池的线程数设置需要看具体执行的任务是什么类型的。
任务类型可以分:CPU 密集型任务和 I/O 密集型任务。
Runtime.getRuntime().availableProcessors()
这个可以查看电脑的CPU核心数
CPU 密集型任务
CPU 密集型任务,就好比单纯的数学计算任务,它不会涉及 I/O 操作,也就是说它可以充分利用 CPU 资源(如果涉及 I/O,在进行 I/O 的时候 CPU 是空闲的),不会因为 I/O 操作被阻塞,因此不需要很多线程,线程多了上下文开销反而会变多。
根据经验法则,CPU 密集型任务线程数 = CPU 核心数 + 1
。
I/O 密集型任务
I/O 密集型任务,有很多 I/O 操作,例如文件的读取、数据库的读取等等,任务在读取这些数据的时候,是无法利用 CPU 的,对应的线程会被阻塞等待 I/O 读取完成,因此如果任务比较多,就需要有更多的线程来执行任务,来提高等待 I/O 时候的 CPU 利用率。
根据经验法则,I/O 密集型任务线程数 = CPU 核心数 * 2
或更多一些。
ini
// 获取 CPU 核心数
int cpuCount = Runtime.getRuntime().availableProcessors();
// CPU 密集型线程池
ExecutorService cpuPool = Executors.newFixedThreadPool(cpuCount + 1);
// I/O 密集型线程池(经验值:核心数 * 2)
ExecutorService ioPool = Executors.newFixedThreadPool(cpuCount * 2);
>以上公式仅是一个纯理论值,仅供参考!在生产上,需要考虑机器的硬件配置,设置预期的 CPU 利用率、CPU负载等因素,再通过实际的测试不断调整得到合理的线程池配置参数。