4:Thread第三部分线程池
ThreadPoolExecutor 是 Java 提供的一个线程池类,可以实现多线程并发处理。它提供了多种可配置的构造参数,以满足不同的业务需求。下面给出一个示例代码,并详细解释一下 ThreadPoolExecutor 构造参数的含义和作用:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
public static void main(String[] args) {
int corePoolSize = 3; // 线程池核心线程数
int maximumPoolSize = 5; // 线程池最大线程数
long keepAliveTime = 60L; // 非核心线程数闲置超时时间,单位为秒
int queueSize = 10; // 任务队列大小
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(queueSize));
for (int i = 1; i <= 10; i++) {
final int taskId = i;
Runnable task = () -> {
System.out.println("Task #" + taskId + " is running by "
+ Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务执行时间,单位为毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task #" + taskId + " is completed by "
+ Thread.currentThread().getName());
};
executor.execute(task);
}
executor.shutdown();
}
}
在上述代码中,我们首先定义了线程池的核心线程数 corePoolSize
、最大线程数 maximumPoolSize
、非核心线程闲置超时时间 keepAliveTime
和任务队列大小 queueSize
,然后使用这些参数创建了 ThreadPoolExecutor 实例。
接下来,在 for 循环中向线程池中提交 10 个任务。每个任务会输出自己的编号和执行线程的名字,然后模拟一个耗时 1 秒的任务执行过程,并在任务完成后再输出一条消息。
ThreadPoolExecutor 构造参数的具体含义如下:
- corePoolSize:线程池的核心线程数,即能同时执行的任务数量;
- maximumPoolSize:线程池的最大线程数,即所有线程池中线程数的上限;
- keepAliveTime:非核心线程数闲置超时时间,即超过核心线程数以外的线程闲置超过一定时间后会被销毁,以节省系统资源;
- unit:keepAliveTime 参数的时间单位,例如 TimeUnit.SECONDS 表示 keepAliveTime 是以秒为单位的;
- workQueue:任务队列,用于存放未执行的任务;
- threadFactory:线程工厂,用于创建新线程;
- handler:饱和策略,当任务队列已满且没有空闲线程时,新提交的任务应该采取怎样的处理方式。
其中,workQueue 和 handler 的类型分别是 BlockingQueue 和 RejectedExecutionHandler,具体实现类可以根据业务需求进行选择。例如,LinkedBlockingQueue 和 ArrayBlockingQueue 都是阻塞队列的实现,但前者具有无限容量,而后者则需要指定固定容量。而 CallerRunsPolicy 策略会将任务交给当前线程来执行,这样可能会对系统造成更大的负担,可以使用 DiscardPolicy 或其他合适的策略来避免这种情况。
4.1、任务队列
任务队列是用于存放还未执行的任务的数据结构,它通常是一个阻塞队列,当线程池中所有的核心线程都在忙碌处理任务时,新提交的任务就会被暂存在任务队列中等待执行。Java 提供了多种类型的阻塞队列,常见的有以下几种:
- ArrayBlockingQueue:一个由数组结构组成的有界队列,按照先进先出的原则对元素进行排序。
- LinkedBlockingQueue:一个由链表结构组成的有界(默认大小是 Integer.MAX_VALUE)队列,按照先进先出的原则对元素进行排序。
- SynchronousQueue:一个不存储元素的队列,每个插入操作必须等待另一个线程调用移除操作,否则插入操作一直处于阻塞状态(直到队列中已有空闲的线程为止)。
- PriorityBlockingQueue:一个支持优先级排序的无界队列。
ArrayBlockingQueue 和 LinkedBlockingQueue 都是基于内存数组和链表实现的有界队列,其中 LinkedBlockingQueue 默认大小为 Integer.MAX_VALUE,意味着队列几乎是无界的。这两种队列的主要区别在于内部数据结构不同,ArrayBlockingQueue 内部是一个数组,而 LinkedBlockingQueue 内部是一个链表。
SynchronousQueue 是一个不存储元素的队列,每次插入操作必须等待另一个线程调用移除操作。这种队列只有在提交给线程池的任务被立即执行时才会被使用。由于 SynchronousQueue 没有任何容量,因此任务无法排队,如果存在空闲线程则直接将任务交给该线程执行,否则就创建一个新线程来执行任务。
PriorityBlockingQueue 是支持优先级排序的队列,它可以根据元素的优先级进行排序,但是并没有固定大小限制,也不存在任何容量或者阻塞性质,因此需要特别注意可能会出现内存溢出的问题。
4.2、线程工厂
线程工厂是用于创建新线程的对象,它提供了一种将线程的创建和线程池的实现分离的方式。通过自定义线程工厂,我们可以更方便地对线程进行管理和监控,并且可以为线程设置名字、优先级等属性。Java 提供了一个 ThreadFactory 接口,用于定义生产线程的规范,常见的实现类有以下几种:
- Executors.defaultThreadFactory() :返回一个由系统默认提供的线程工厂创建的新线程。
- ThreadFactoryBuilder :基于构建者模式实现的自定义线程工厂,可以灵活设置线程名前缀、是否守护进程等属性。
- NamedThreadFactory:一个通用的线程工厂实现类,支持自定义线程名和优先级。
4.3、饱和策略
饱和策略是线程池在无法处理新提交的任务时采取的一种策略,Java 提供了 4 种饱和策略,分别是:
- AbortPolicy :直接抛出异常,默认策略;
- CallerRunsPolicy :只用调用者所在线程来运行任务;
- DiscardOldestPolicy :丢弃队列中最老的任务,并尝试重新提交当前任务;
- DiscardPolicy :直接丢弃任务。
以上这四种策略都实现了 RejectedExecutionHandler 接口,可以根据实际业务需求选择合适的策略。通常来说,如果不想丢失任何任务,可以使用 CallerRunsPolicy 策略;如果对性能有要求,应该控制队列大小并使用 DiscardPolicy 或 DiscardOldestPolicy 策略来丢弃一定量的任务。