线程池(ThreadPool)是一种通过预先创建和维护一组线程的机制,用来高效管理并发任务。线程池不仅能减少创建和销毁线程的开销,还能更好地控制并发任务的执行。Java 中提供了多种方式来管理线程池,其中包括 ExecutorService
和 ThreadPoolExecutor
等关键类。
一、线程池的基本概念
线程池的核心思想是维护一组可复用的线程来执行任务,而不是为每个任务创建新线程。这样可以避免频繁的线程创建和销毁,提升系统性能。线程池的主要好处包括:
- 减少资源消耗:通过复用线程,避免了频繁创建和销毁线程的成本。
- 提升响应速度:任务不需要等待新线程的创建,因为线程池中已经有线程可用。
- 控制并发数:线程池可以通过限制线程的数量,控制系统中并发任务的数量,防止资源耗尽。
- 任务管理:线程池提供了任务队列,可以管理提交的任务,支持任务的调度和优先级。
Java 的 Executor
框架为开发者提供了一种标准化的方式来使用线程池,它将任务的提交与任务的执行分离,使得开发者无需关心底层线程的管理细节。
二、ExecutorService 和 ThreadPoolExecutor 的使用
ExecutorService
是 java.util.concurrent
包中的一个接口,提供了一组用于管理线程池的抽象方法。ThreadPoolExecutor
是 ExecutorService
接口的一个实现类,提供了线程池的核心功能。
1. 使用 ExecutorService
ExecutorService
是线程池的上层接口,可以通过静态方法 Executors
创建常用的线程池。以下是几种常用的线程池类型:
-
固定线程池(FixedThreadPool):创建一个固定大小的线程池,线程池中始终有指定数量的线程在运行。新任务提交后,如果线程池中所有线程都在执行任务,则新任务会进入任务队列等待。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
-
缓存线程池(CachedThreadPool):创建一个缓存线程池。线程池的大小不固定,随着需求动态增长。线程闲置时会被回收,适合执行大量短期任务。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
-
单线程池(SingleThreadExecutor):创建一个只有单个线程的线程池,所有任务将在同一个线程中顺序执行。
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
-
调度线程池(ScheduledThreadPool):用于定时或周期性执行任务。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
2. 提交任务
任务可以通过 ExecutorService
提交执行,通常有以下几种方法:
-
execute(Runnable)
:提交一个Runnable
任务,执行没有返回结果。fixedThreadPool.execute(() -> { System.out.println("Task executed by thread pool"); });
-
submit(Runnable)
和submit(Callable)
:提交任务并返回Future
对象,Future
可用于获取任务执行结果。Future<String> future = fixedThreadPool.submit(() -> { return "Task result"; });
3. 关闭线程池
使用完线程池后,应该关闭它以释放资源。可以调用以下方法关闭线程池:
-
shutdown()
:会等待线程池中的任务执行完毕后再关闭。fixedThreadPool.shutdown();
-
shutdownNow()
:立即停止所有任务,并尝试中断正在执行的线程。fixedThreadPool.shutdownNow();
三、ThreadPoolExecutor 的底层实现原理
ThreadPoolExecutor
是 Java 线程池的核心实现类,它提供了高度可配置的线程池管理机制。我们可以通过构造方法自定义线程池的核心参数,如线程数、队列类型、拒绝策略等。其构造方法如下:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程的存活时间
TimeUnit unit, // 存活时间的单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂,用于创建新线程
RejectedExecutionHandler handler // 拒绝策略
)
1. 核心参数介绍
-
corePoolSize:核心线程数,当提交任务时,如果当前线程数小于核心线程数,即使线程池中有空闲线程,也会创建新的线程来执行任务。
-
maximumPoolSize:线程池的最大线程数,当任务队列已满且线程数小于最大线程数时,线程池会创建新线程来执行任务。
-
keepAliveTime:非核心线程的空闲时间超过此时间后将被终止(如果当前线程数大于
corePoolSize
)。 -
workQueue:任务队列,用于存放等待执行的任务。常用的任务队列类型有:
ArrayBlockingQueue
:有界队列,任务达到队列上限时,新的任务将被阻塞或拒绝。LinkedBlockingQueue
:无界队列,适合任务量较大的场景。SynchronousQueue
:没有容量的队列,提交的任务必须直接交给线程执行,否则会创建新线程。
-
RejectedExecutionHandler:当线程池和任务队列都满时,新的任务将被拒绝。可以选择不同的拒绝策略:
AbortPolicy
:默认策略,直接抛出RejectedExecutionException
异常。CallerRunsPolicy
:调用执行任务的线程(通常是提交任务的线程)来执行任务。DiscardPolicy
:直接丢弃任务,不抛异常。DiscardOldestPolicy
:丢弃队列中最旧的任务,然后重新尝试执行当前任务。
2. 工作流程
ThreadPoolExecutor
的工作流程大致可以分为以下几个步骤:
-
任务提交:当任务通过
execute()
或submit()
方法提交时,线程池首先会检查当前线程数是否少于corePoolSize
,如果是,则创建新的线程执行任务。 -
任务入队:如果线程数已经达到
corePoolSize
,任务会被放入workQueue
队列中等待执行。 -
创建新线程:当队列已满且线程数未达到
maximumPoolSize
,线程池会创建新的线程来执行任务。 -
拒绝策略:如果线程数已经达到
maximumPoolSize
且队列已满,线程池会根据RejectedExecutionHandler
策略拒绝新提交的任务。 -
任务执行:线程从任务队列中取出任务并执行,任务执行完毕后,线程不会立即销毁,而是保持存活状态,等待下一个任务。如果线程的空闲时间超过
keepAliveTime
,并且当前线程数超过corePoolSize
,该线程将被终止。 -
线程销毁:当线程池中的所有线程都处于空闲状态且线程数大于
corePoolSize
时,线程池会根据keepAliveTime
逐步销毁多余的线程,直到只剩下核心线程。
3. 任务队列与线程数的关系
ThreadPoolExecutor
的任务处理策略基于任务队列和线程数的配置,可以分为以下几种情况:
- 如果当前线程数小于
corePoolSize
,则即使队列不满,线程池也会优先创建新线程。 - 如果队列已满且线程数小于
maximumPoolSize
,线程池会继续创建新线程来执行任务。 - 如果队列已满且线程数已达到
maximumPoolSize
,任务将被拒绝,触发拒绝策略。
四、总结
Java 中的 ExecutorService
和 ThreadPoolExecutor
提供了一种标准化、可扩展的线程池管理机制,可以高效管理并发任务。通过线程池,开发者能够减少线程频繁创建和销毁带来的开销,控制并发数,提升系统性能。
线程池的底层实现主要基于核心线程数、最大线程数、任务队列、存活时间以及拒绝策略等参数的组合。通过合理配置线程池,可以在不同场景下实现性能优化和资源控制。