1 ThreadPoolExecutor简介
1.1 应用场景
在Java的并发编程中,我们可以显示创建线程来并发处理任务,示例如下:
new Thread(() -> {
int i = 1;
System.out.println("线程开始工作...");
while (!isOver) {
try {
Thread.sleep(100);
System.out.println("线程执行第" + i + "次");
i++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程结束工作");
}).start();
但为了合理控制创建线程的数量和优化资源开销,可以通过线程池提供线程资源,不在业务代码中显示创建线程。使用线程池创建线程的优点如下:
- 使线程的创建更加规范,可以合理控制开辟线程的数量;
- 线程的细节管理交给线程池处理;
- 在执行大量异步任务时,由于减少了每个任务的调用开销,再加上线程池提供了一种限制和管理资源和线程的方法,能够明显提升性能;
- 方便统计信息,每个ThreadPoolExecutor可以保存一些基本的统计信息,例如完成的任务数量。
1.2 构造方法
ThreadPoolExecutor的4种构造方法如下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
参数解析如下:
参数名 | 类型 | 定义 |
---|---|---|
corePoolSize | int | 核心线程池大小。 |
maximumPoolSize | int | 最大线程池大小。 |
keepAliveTime | long | 线程的最大空闲时间。 |
unit | TimeUnit | 等待时间单位。 |
workQueue | BlockingQueue<Runnable> | 线程阻塞队列。 |
threadFactory | ThreadFactory | 线程创建工厂。 |
handler | RejectedExecutionHandler | 线程拒绝策略。 |
通常,核心和最大的线程池大小仅在构建线程池时设置,但也可以使用setCorePoolSize
和setMaximumPoolSize
方法动态修改。
1.3 预定义线程池
为简化线程池的使用方法,JDK提供了一些预定义线程池,减少了构造方法参数。3种常见预定义线程池的构造方法如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
3种预定义线程池的用法和对比如下:
线程池 | 描述 | 线程池大小 | 等待时间 | 阻塞队列 | 适用场景 |
---|---|---|---|---|---|
FixedThreadPool | 无界线程池,自动线程回收 | 线程全为核心线程,是固定大小的线程池。 | keepAliveTime参数对核心线程无效。 | workQueue为LinkedBlockingQueue(无界阻塞队列),队列最大值为Integer.MAX_VALUE,如果任务提交速度持续大于任务处理速度,会造成大量任务阻塞,可能在执行拒绝策略前,内存溢出。 | 可用于Web服务瞬时削峰,但需注意长时间持续高峰情况造成的队列阻塞。 |
CachedThreadPool | 固定大小的线程池 | 无核心线程池,队列大小为Integer.MAX_VALUE,是缓存线程池,线程会消费。 | 线程等待60s后自动结束。 | workQueue为 SynchronousQueue同步队列,入队出队必须同时传递,且线程创建无限制。 | 快速处理大量耗时较短的任务,如Netty的NIO接受请求。 |
SingleThreadExecutor | 单一后台线程 | 单例线程池,只有1个核心线程。 | 同FixedThreadPool。 | 同FixedThreadPool。 | 常用于需要单例线程处理的场景。 |
需要注意的是,线程池最好不要使用Executors去创建,而要使用ThreadPoolExecutor。主要原因如下:
- JDK中的Executor框架虽然提供了
newFixedThreadPool()
、newSingleThreadExecutor()
和newCachedThreadPool()
等预定义线程池,但都有局限性,不够灵活; - 由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于明确线程池的运行规则,创建符合业务场景需要的线程池,避免资源耗尽的风险。
2 简单应用
在实际应用中,ThreadPoolExecutor线程池的创建、线程执行顺序和数量限制有以下特点:
- 进入核心线程池的线程将立即开始执行;
- 线程数超过核心线程池大小后,后续线程优先进入阻塞队列,阻塞队列满后,再进入线程池开始执行,直到数量到达最大线程池大小;
- 阻塞队列满后,线程池还能接收的线程数量为:最大线程池大小 - 核心线程池大小;
- 线程池能容纳的最大线程数量为:最大线程池大小 + 阻塞队列容量,线程数量超过该值后,后续加入的线程将触发拒绝策略;
- allowCoreThreadTimeOut的默认值为false,调用
allowCoreThreadTimeOut(true)
方法后,超过线程最大空闲时间的线程将被销毁。
任务类示例代码如下:
public class WorkerThread implements Runnable {
private int id;
public WorkerThread(int id) {
this.id = id;
}
@Override
public void run() {
try {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.sss");
System.out.println(format.format(new Date()) + " | The thread " + id + " starts working.");
Thread.sleep(3000);
System.out.println(format.format(new Date()) + " | The thread " + id + " is down.");
} catch <