1. 情景与定义
-
情急:
想象一下:你是一个初出茅庐的快递公司老板,开始时快递不是很多,时有时无,于是在需要送快递时你只招聘一个临时员工,送完快递后,不再需要就把员工开除了,等下次需要送快递时在重新招聘(模拟线程的创建和销毁)。渐渐的,你的公司小有名气,快递也越来越多,这时你发现如果按照之前的做法招聘员工会极大的拖慢公司的运营效率,因为每次重新招聘员工需要大量的时间和精力,聪明的你就想到:我可以一次性招聘足够多的正式员工,快递少的时候,这些正式员工就能维持公司的运营,快递多的时候,再招聘一些临时员工缓解公司的运营压力,等快递少的时候再把这些临时工开除。
这种运营策略不仅能够维持公司的运营,而且可以极大地节省时间和精力。在多线程中,我们把这种运营策略称为“线程池”。
-
定义:
线程池(Thread Pool)是一种基于池化技术设计的并发框架,用于管理和复用线程资源。它预先创建一定数量的线程(正式员工),并将这些线程放入一个“池”中(公司),当有任务(快递)需要执行时,线程池会从中取出一个空闲的线程来执行该任务,而不是每次都创建一个新的线程,当程序中的任务很多,线程池中的线程不足以完成所有任务时,线程池会创建一些临时线程来应对这一情况。这种方式可以显著提高程序的执行效率和性能,同时减少资源消耗。
2. 标准库中的线程池
2.1)ThreadPoolExecutor
ThreadPoolExecutor
是 Java 并发包(java.util.concurrent
)中的一个非常有用的类。
主要特点
- 线程复用:减少了线程创建和销毁的开销。
- 控制并发数:通过线程池的大小来控制同时执行的线程数量,从而控制并发级别。
- 管理任务队列:未执行的任务被存储在任务队列中,等待线程执行。
- 提供灵活的关闭策略:可以优雅地关闭线程池,等待所有任务执行完成后再关闭线程池。
构造方法(面试考点)
方法参数:
ThreadPoolExecutor
类提供了多个构造方法,但最基本的是下面这个:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:核心线程数,即线程池维护线程的最少数量。
- maximumPoolSize:最大线程数,线程池允许的最大线程数。
- keepAliveTime 和 unit:非核心线程空闲时的存活时间,以及时间单位。
- workQueue:用于保存等待执行的任务的阻塞队列。
- threadFactory:用于创建新线程的线程工厂。
- handler:当任务太多来不及处理时,用于处理拒绝任务的策略。
如何正确配置:
-
1. corePoolSize(核心线程数)
- 定义:线程池中保持存活的最小线程数。
- 设置方法:根据任务类型(CPU密集型或IO密集型)和CPU核心数来设置。
- CPU密集型:线程数一般设置为CPU核心数,以避免线程上下文切换的开销。(详细解释请看文末)
- IO密集型:线程数可以设置为CPU核心数的两倍或更多,具体取决于IO操作的等待时间。
- 可以通过构造函数直接设置,或使用
setCorePoolSize(int corePoolSize)
方法动态调整。
-
2. maximumPoolSize(最大线程数)
- 定义:线程池中允许的最大线程数。
- 设置方法:根据系统的负载和任务类型来设置。
- 当任务队列满且已创建的线程数小于
maximumPoolSize
时,线程池会创建新线程来处理任务。 - 可以通过构造函数直接设置,或使用
setMaximumPoolSize(int maximumPoolSize)
方法动态调整。
- 当任务队列满且已创建的线程数小于
-
3. keepAliveTime(线程空闲时间)
- 定义:非核心线程在空闲状态下的存活时间。
- 设置方法:根据任务频率和响应时间要求来设置。
- 当线程空闲时间超过
keepAliveTime
时,非核心线程将被终止。 - 可以通过构造函数直接设置,或使用
setKeepAliveTime(long time, TimeUnit unit)
方法调整。
- 当线程空闲时间超过
-
4. unit(时间单位)
- 定义:
keepAliveTime
的时间单位。 - 设置方法:与
keepAliveTime
一起设置,常用的时间单位有TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
等。
- 定义:
-
5. workQueue(任务队列)
- 定义:用于保存等待执行的任务的阻塞队列。
- 设置方法:根据任务类型和需求选择合适的队列类型。
- ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序。
- LinkedBlockingQueue:基于链表的无界阻塞队列(也可以设置为有界)。
- SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,反之亦然。
- PriorityBlockingQueue:具有优先级的无界阻塞队列。
-
6. threadFactory(线程工厂)
- 定义:用于创建新线程的工厂,可以定制线程的属性,如线程名、是否为守护线程等。
- 设置方法:一般通过调用
Executors.defaultThreadFactory()
设置为默认,也可以通过实现ThreadFactory
接口来自定义线程工厂,并在构造函数中传入。
-
7. rejectedExecutionHandler(任务拒绝处理器)
- 定义:当线程池和队列都满时,用于处理新任务的策略。
- 设置方法:通过实现
RejectedExecutionHandler
接口来自定义拒绝策略,或在构造函数中指定JDK提供的几种策略。- AbortPolicy:默认策略,直接抛出
RejectedExecutionException
。 - CallerRunsPolicy:用调用者所在的线程来执行任务。
- DiscardOldestPolicy:丢弃队列中最早的任务,并执行当前任务。
- DiscardPolicy:直接丢弃任务,不抛出异常也不执行。
- AbortPolicy:默认策略,直接抛出
使用示例
以下是一个简单的 ThreadPoolExecutor
使用示例:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPoolExecutorExample {
public static void main(String[] args) {
// 创建任务队列
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
1, // 非核心线程的空闲存活时间
TimeUnit.SECONDS, // 存活时间单位
queue, // 任务队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
// 提交任务
for (int i = 0; i < 15; i++) {
int taskId = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " is processing " + taskId);
try {
Thread.sleep(1000); // 模拟耗时任务
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池
executor.shutdown();
try {
// 等待所有任务执行完成
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// 取消当前正在执行的任务
executor.shutdownNow();
}
} catch (InterruptedException e) {
// 当前线程在等待过程中被中断
executor.shutdownNow();
}
}
}
注意事项
- 合理使用线程池大小,避免资源浪费或系统过载。
- 谨慎处理拒绝策略,确保系统稳定性。
- 优雅地关闭线程池,等待所有任务完成后再关闭。
- 注意线程安全问题,尤其是在共享资源时。
2.2) Executors 类
Executors 类是 Java 并发编程中的一个非常重要的工具类,它位于 java.util.concurrent
包中。Executors 类提供了一系列的静态工厂方法,用于创建不同类型的线程池(Executor),这些线程池可以帮助我们更方便地管理线程的生命周期,提高程序的性能和可维护性。
Executors 类创建的线程池类型主要包括以下几种:
-
newCachedThreadPool():
- 创建一个可缓存的线程池,如果线程池中的线程数量超过了处理任务所需要的线程,那么它就会回收空闲(60秒无任务执行)的线程,当有新任务提交时,它会创建新线程或者复用空闲线程来执行任务。这种线程池适用于执行大量短期异步任务。
-
newFixedThreadPool(int nThreads):
- 创建一个固定大小的线程池,可以包含指定数量的线程。这种线程池中的线程数量是固定的,即使有空闲线程,如果线程池已经满了,也不会再创建新的线程,而是将任务放在队列中等待执行。这种线程池适用于执行固定数量的长期任务。
-
newSingleThreadExecutor():
- 创建一个单线程的线程池,这个线程池中只有一个线程来执行任务,所有的任务都按照提交的顺序串行执行。这种线程池适用于需要按顺序执行任务的场景。
-
newScheduledThreadPool(int corePoolSize):
- 创建一个固定大小的线程池,用于定时或周期性地执行任务。与
newFixedThreadPool
类似,但它支持更多的定时和周期性任务执行的功能。
- 创建一个固定大小的线程池,用于定时或周期性地执行任务。与
-
newSingleThreadScheduledExecutor():
- 创建一个单线程的定时执行线程池,与
newScheduledThreadPool
类似,但它只包含一个线程,用于串行定时执行任务。
- 创建一个单线程的定时执行线程池,与
-
newWorkStealingPool(int parallelism):
- 创建一个工作窃取线程池,线程数量根据 CPU 核心数动态调整。这种线程池适用于 CPU 密集型的任务,通过减少线程间的竞争和上下文切换来提高性能。
Executors 类通过封装 ThreadPoolExecutor 的复杂性,提供了更加简洁的 API 来创建和管理线程池。然而,值得注意的是,直接使用 Executors 类的某些方法(如 newCachedThreadPool
和 newFixedThreadPool
)可能会带来一些潜在的问题,如资源耗尽等。因此,在实际开发中,建议根据具体需求选择合适的线程池类型,并考虑使用 ThreadPoolExecutor 的构造函数来创建线程池,以便更精确地控制线程池的参数和行为。
总的来说,Executors 类是 Java 并发编程中一个非常有用的工具类,它极大地简化了线程池的创建和管理过程,使得开发者能够更专注于任务的提交和执行。