Java 线程池(Thread Pool)是一种预先创建和管理一组线程的机制,用于优化多线程编程中的线程创建和销毁成本。线程池可以重复使用这些线程来执行多个任务,从而提高资源利用率、降低系统开销,并且避免过多线程造成的资源竞争和性能问题。
Java 提供了内置的线程池支持,主要通过 java.util.concurrent
包中的 Executor 框架 来实现线程池的管理和任务调度。
1. Java 线程池的核心概念
- 线程复用:线程池通过复用已有的线程来避免反复创建和销毁线程,减少线程的创建开销。
- 任务调度:通过线程池提交任务,任务被自动分配给空闲线程执行,无需手动创建线程。
- 资源控制:通过控制线程池中线程的数量,避免线程过多导致系统资源耗尽。
- 任务排队:当线程池中的线程被占用时,新的任务会进入队列等待空闲线程执行。
2. Java 线程池的基本实现
Java 中使用 Executor 框架来实现线程池。主要的接口和类包括:
- Executor 接口:是所有执行任务的基础接口,提供
execute()
方法来提交任务。 - ExecutorService 接口:是
Executor
的子接口,增加了管理线程池生命周期和获取任务执行结果的方法,如submit()
、shutdown()
、invokeAll()
等。 - ThreadPoolExecutor 类:是最常用的线程池实现类,支持灵活的线程池配置。
3. Java 提供的常用线程池类型
Java 的 Executors
工具类提供了几种常用的线程池工厂方法。常见的线程池类型包括:
1. FixedThreadPool(固定大小线程池)
- 创建方式:
Executors.newFixedThreadPool(int nThreads)
- 特点:
- 创建一个包含固定数量线程的线程池。
- 当所有线程都在忙碌时,新的任务将进入等待队列,直到有空闲线程。
- 使用场景:
- 适合执行长期存在且固定数量的并发任务场景。
- 使用在需要限制并发线程数的场景,以避免过多线程导致的系统资源消耗。
- 示例:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); fixedThreadPool.execute(() -> { // 任务代码 });
2. CachedThreadPool(缓存线程池)
- 创建方式:
Executors.newCachedThreadPool()
- 特点:
- 线程池的线程数量不固定,根据需要动态创建新线程,已完成的线程会被回收重用。
- 当任务非常多且执行时间较短时,可以避免频繁创建线程。
- 如果线程池中的线程长时间闲置,它们将会被终止并移除。
- 使用场景:
- 适用于执行大量短期异步任务,且不确定任务数量的场景。
- 缓存线程池能够快速响应大批量的任务请求。
- 示例:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); cachedThreadPool.execute(() -> { // 任务代码 });
3. SingleThreadExecutor(单线程池)
- 创建方式:
Executors.newSingleThreadExecutor()
- 特点:
- 线程池中只有一个线程,所有任务按顺序执行。
- 适合需要保证任务执行顺序的场景。
- 即使线程出现异常,也会保证线程重新启动执行后续任务。
- 使用场景:
- 适用于需要确保任务按顺序执行的场景,例如日志处理、后台任务等。
- 示例:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); singleThreadExecutor.execute(() -> { // 任务代码 });
4. ScheduledThreadPool(定时任务线程池)
- 创建方式:
Executors.newScheduledThreadPool(int corePoolSize)
- 特点:
- 线程池用于调度执行定时任务,或者周期性执行任务。
- 可以指定延迟时间执行任务,也可以指定周期性重复执行任务。
- 使用场景:
- 适用于需要周期性或延时执行任务的场景,如定时任务调度器。
- 示例:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); scheduledThreadPool.schedule(() -> { // 延迟执行的任务代码 }, 5, TimeUnit.SECONDS); scheduledThreadPool.scheduleAtFixedRate(() -> { // 每隔一定时间重复执行的任务 }, 0, 10, TimeUnit.SECONDS);
5. WorkStealingPool(工作窃取线程池,JDK 8 引入)
- 创建方式:
Executors.newWorkStealingPool(int parallelism)
- 特点:
- 使用
ForkJoinPool
作为底层实现,支持并行处理。 - 线程池根据任务的不同工作量,允许线程从其他队列“窃取”任务,达到任务负载均衡。
- 任务可以并行执行,特别适合分治算法(Divide and Conquer)。
- 使用
- 使用场景:
- 适用于 CPU 密集型任务,特别是需要进行大量并行计算的场景。
- 示例:
ExecutorService workStealingPool = Executors.newWorkStealingPool();
4. 如何选择合适的线程池
根据不同的应用场景选择合适的线程池至关重要,以下是常见的使用建议:
- 固定数量的长期任务:选择 FixedThreadPool。适用于系统中有固定数量并发任务,任务执行时间较长的场景。可以控制线程数量,避免系统资源耗尽。
- 大量短期异步任务:选择 CachedThreadPool。适用于大量并发的短期任务,特别是任务执行时间较短、数量不确定的场景。
- 任务必须按顺序执行:选择 SingleThreadExecutor。适合单一后台任务的场景,需要保证任务顺序执行。
- 定时任务:选择 ScheduledThreadPool。适合需要定时执行或周期性执行的任务,例如调度器、定时任务等。
- 并行处理或 CPU 密集型任务:选择 WorkStealingPool。适合处理并行任务,充分利用多核 CPU 的优势,尤其是需要分治算法的场景。
5. 自定义 ThreadPoolExecutor
在某些复杂场景下,可能需要根据需求自定义线程池的参数,可以通过 ThreadPoolExecutor
类来自定义线程池。ThreadPoolExecutor
支持设置核心线程数、最大线程数、任务队列、拒绝策略等。
示例代码:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, // 核心线程数
maximumPoolSize, // 最大线程数
keepAliveTime, // 线程空闲时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(queueCapacity), // 任务队列
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
executor.execute(() -> {
// 任务代码
});
自定义参数解释:
- corePoolSize:核心线程数,线程池在空闲时也会保留的最小线程数。
- maximumPoolSize:线程池允许的最大线程数。
- keepAliveTime:当线程池中的线程数超过
corePoolSize
时,多余线程的最大空闲时间,超过此时间多余线程将会被销毁。 - 任务队列:用于存储等待执行的任务的队列。
- 拒绝策略:当线程池中的任务队列已满,且没有空闲线程时执行的策略。
拒绝策略包括:
- AbortPolicy(默认):抛出
RejectedExecutionException
异常。 - CallerRunsPolicy:由调用线程执行任务。
- DiscardPolicy:直接丢弃任务,不抛出异常。
- DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试提交新的任务。
总结
Java 线程池是一种高效的多线程管理机制,可以根据不同的应用场景选择合适的线程池类型。通过合理使用线程池,不仅可以提升程序的并发性能,还能有效管理系统资源,防止过多线程导致的资源竞争和系统崩溃。
ThreadPoolExecutor
是 Java 中自定义线程池的核心类,允许我们灵活地设置线程池的行为和性能参数。要理解 ThreadPoolExecutor
的工作原理,关键在于弄清楚以下几个主要参数的含义和作用:
- corePoolSize(核心线程数)
- maximumPoolSize(最大线程数)
- keepAliveTime(线程存活时间)
- unit(存活时间单位)
- workQueue(任务队列)
- threadFactory(线程工厂)
- handler(拒绝策略)
1. corePoolSize(核心线程数)
corePoolSize
表示线程池中始终保持存活的最小线程数量。即使这些线程处于空闲状态,线程池也不会将其销毁,除非线程池被关闭。
- 当提交的任务数量少于或等于
corePoolSize
时,线程池直接创建新线程执行任务。 - 当任务数量大于
corePoolSize
且小于maximumPoolSize
时,线程池会先将任务放入任务队列,等有空闲的线程时再从队列中取出任务执行。
示例:
如果 corePoolSize=5
,即使没有任务需要执行,线程池也会保持 5 个空闲线程等待新任务的到来。
2. maximumPoolSize(最大线程数)
maximumPoolSize
表示线程池中允许创建的最大线程数。当任务队列已满且线程池中的线程数已达到 corePoolSize
,但还有新任务提交时,线程池可以继续创建线程,直到线程总数达到 maximumPoolSize
。
- 当线程数达到
maximumPoolSize
且任务队列已满,再有新任务提交时,线程池将根据拒绝策略(handler
)处理新任务。
示例:
如果 maximumPoolSize=10
,当线程池中的线程数达到 10 时,任何新提交的任务都将根据拒绝策略进行处理。
3. keepAliveTime(线程存活时间)
keepAliveTime
指的是当线程池中的线程数量超过 corePoolSize
时,多余的线程在空闲状态下能够保持存活的时间。如果超过这个时间且没有新任务到来,这些多余的线程将会被销毁。
- 仅当线程数超过
corePoolSize
时,keepAliveTime
才起作用,用于控制多余线程的销毁时机。 - 如果将
allowCoreThreadTimeOut(true)
设置为true
,则keepAliveTime
也会作用于核心线程,允许核心线程在空闲时被回收。
示例:
如果 keepAliveTime=60
,并且线程池中的线程数大于 corePoolSize
,那么超过 corePoolSize
的线程在空闲 60 秒后会被销毁。
4. unit(存活时间单位)
unit
是 keepAliveTime
的时间单位,可以是以下几种类型:
TimeUnit.MILLISECONDS
:毫秒TimeUnit.SECONDS
:秒TimeUnit.MINUTES
:分钟TimeUnit.HOURS
:小时TimeUnit.DAYS
:天
示例:
如果 keepAliveTime=60
,unit=TimeUnit.SECONDS
,那么线程的存活时间就是 60 秒。
5. workQueue(任务队列)
workQueue
是一个用来保存等待执行任务的阻塞队列,当线程池中的线程数量达到 corePoolSize
后,新的任务会被放入 workQueue
中等待执行。
常见的任务队列有以下几种类型:
- SynchronousQueue:一个不存储任务的队列,每个插入操作必须等到有线程执行,否则无法继续插入。这种队列适合任务数量很多且执行时间很短的场景。
- LinkedBlockingQueue:基于链表的有界阻塞队列,未设置容量时默认是无界队列。适合任务多但不需要无限扩展线程的场景。
- ArrayBlockingQueue:基于数组的有界阻塞队列,需要设置容量,适合对任务数量有明确限制的场景。
示例:
如果使用 LinkedBlockingQueue
,且队列容量为 100,那么当线程池中的线程数已达到 corePoolSize
时,最多可以再提交 100 个任务到队列中等待执行。
6. threadFactory(线程工厂)
threadFactory
用来创建线程池中的线程,通常用于给线程指定一些属性,如线程的名称、是否为守护线程等。默认使用 Executors.defaultThreadFactory()
。
示例:
通过自定义 ThreadFactory
来给每个线程指定名称:
ThreadFactory namedThreadFactory = new ThreadFactory() {
private int count = 0;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "Thread-" + count++);
}
};
7. handler(拒绝策略)
handler
是拒绝策略,当线程池已经达到 maximumPoolSize
且任务队列已满时,如何处理新提交的任务。常见的拒绝策略包括:
- AbortPolicy(默认):抛出
RejectedExecutionException
,拒绝任务。 - CallerRunsPolicy:让调用线程执行任务,减缓任务提交速度。
- DiscardPolicy:直接丢弃任务,不抛出异常。
- DiscardOldestPolicy:丢弃队列中等待最久的任务,然后重新尝试提交任务。
示例:
如果设置拒绝策略为 CallerRunsPolicy
,当线程池无法处理新任务时,调用线程(提交任务的线程)会直接执行该任务:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
综合示例
假设我们创建一个线程池,核心线程数为 5,最大线程数为 10,任务队列的容量为 100,空闲线程的存活时间为 60 秒,并且使用 CallerRunsPolicy
作为拒绝策略:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS, // unit
new ArrayBlockingQueue<>(100), // workQueue
Executors.defaultThreadFactory(), // threadFactory
new ThreadPoolExecutor.CallerRunsPolicy() // handler
);
在这个例子中:
-
如果任务数量不超过 5 个,线程池直接分配线程执行任务。
-
如果任务数量超过 5 个且小于等于 105(线程池最大为 10 个线程,队列可容纳 100 个任务),任务会被放入队列。
根据这些参数,线程池最多可以同时处理的任务数量为:
- 核心线程处理的任务:5个(这些线程始终是活跃的)
- 队列中的任务:最多100个(这些任务在等待被处理)
- 额外线程:当任务数量超过100个时,可以创建额外的线程,最多可创建5个额外线程(因为最大线程数为10)。
因此,最多可以处理的任务数量为:
最多任务数 = 核心线程数+ 任务队列容量+ 额外线程数
最多任务数= 5 + 100 + 5 = 110
所以,最多可以处理110个任务。在任务数超过110个时,线程池将根据配置选择拒绝策略来处理额外的任务。
总结
ThreadPoolExecutor
的核心参数通过灵活组合,可以精细控制线程池的行为和性能,适用于不同场景的并发任务需求。