一、概述
1.1 为什么要使用线程池
一言以蔽之:节约服务器资源。线程资源多宝贵啊,当然是能复用就复用。
1.2 线程池的基本组成部分
- 线程池管理器:用于创建与管理线程池,包括创建线程池、销毁线程池、添加任务
- 工作线程:线程池中干活的线程,没有任务时处于等待状态,可以重复利用以完成任务。
- 任务接口:每个提交到线程池到任务必须实现的接口,相当于一个规范,规定了任务的入口、执行完的收尾工作、任务的执行状态等。
- 任务队列:用于存放提交到线程池等任务,起到一个缓冲作用,并不是所有的任务一提交到线程池就会立即执行的。
各部分的职能大致如下图所示:
1.3 线程池中任务的执行过程
新建线程池时,需要指定如下几个参数:
- 工作线程数量大小:线程池空闲时持有的线程数量(线程池刚创建时数量为0)
- 最大线程数量:就线程池最大持有的线程数量
- 工作队列:用于存放提交到线程池等任务,不指定大小则默认为Integer.MAX_VALUE
- (可选)拒绝执行策略:在任务队列已满、最大线程数量已满的情况下,线程池会直接拒绝执行后面提及过来的任务,这时,我们可以人为地处理一下这些被拒绝执行的任务(可以提示用户或记录日志等)
基于上述参数以及参数说明,线程池中的任务执行可以用下图来描述:
1.4 线程池API
1.4.1 相关类与接口
| 类型 | 名称 | 描述 |
|---|---|---|
| 接口 | Executor | 最顶级接口,定义了执行任务的execute方法 |
| 接口 | ExecutorService | 继承了Executor接口,扩展了Callable、Future接口,新增了关闭方法 |
| 接口 | ScheduledExecutorService | 继承了ExecutorService接口,新增定义了有关定时任务的方法 |
| 实现类 | ThreadPoolExecutor | 最基础、最常用的线程池实现 |
| 实现类 | ScheduledThreadPoolExecutor | 继承了ThreadPoolExecutor类,实现了ScheduledExecutorService相关方法 |
1.4.2 Executors工具类
在实际写代码的时候,我们可以自己使用上面说到的ThreadPoolExecutor、ScheduledThreadPoolExecutor来创建线程池,也可以使用Executors工厂类来新建如下几种特殊的线程池:
- Executors.newFixedThreadPool(int nThreads):创建一个固定大小、工作队列无限大的线程池,核心线程数 = 最大线程数;
- Executors.newCachedThreadPool():核心线程数量0,最大数量Integer.MAX_VALUE,任务队列是SynchronousQueue队列,超出核心线程数量的线程存活时间:60秒
- Executors.newSingleThreadPool():创建一个只有一个线程、工作队列无限大的线程池,所有提交过来的任务都会由这唯一的一个线程依次执行。如果执行的过程中该线程异常退出,则会新建一个线程继续执行其他的任务。
- Executors.newScheduledThreadPool(int nThreads):创建一个固定大小、工作队列无限大、能执行定时任务的线程池,任务队列是DelayedWorkQueue延时队列,超出核心线程数量的线程存活时间:0秒
1.4.3 中止线程池
- 优雅关闭:
threadPoolExecutor.shutdown();,已在任务队列中未完成的任务将会继续执行,但新的任务将无法追加。所有任务执行完毕后,线程池才会销毁。 - 暴力关闭:
List<Runnable> shutdownNow = threadPoolExecutor.shutdownNow();,返回的是已在任务队列中未完成的任务,这些任务将会被立即中止(导致异常),线程池会被立即销毁。
二、小试牛刀:多种场景下线程池的使用
2.1 以下示例(除了定时任务相关线程池)将共用如下基础代码(给指定线程池对象提交5个任务):
public void testCommon(ThreadPoolExecutor threadPoolExecutor) throws Exception {
for (int i = 0; i < 5; i++) {
int n = i;
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("开始执行:" + n);
// 这里的sleep方法并不会阻塞submit方法
Thread.sleep(1000L);
System.err.println("执行结束:" + n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("任务提交成功 :" + i);
}
// 查看线程数量,查看队列等待数量
Thread.sleep(100L);
System.out.println("当前线程池中的线程数量为:" + threadPoolExecutor.getPoolSize());
System.out.println("当前任务队列中的任务数量为:" + threadPoolExecutor.getQueue().size());
System.err.println("-------------------");
// 等待15秒,查看线程数量和队列数量(理论上,会被超出核心线程数量的线程自动销毁)
Thread.sleep(6000L);
System.out.println("当前线程池中的线程数量为:" + threadPoolExecutor.getPoolSize());
System.out.println("当前任务队列中的任务数量为:" + threadPoolExecutor.getQueue().size());
}
2.2 线程池中的核心线程数与最大线程数
// 预计结果:线程池线程数量为2,超出数量的任务,其他的进入队列中等待被执行
private void threadPoolExecutorTest() throws Exception {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>());
testCommon(threadPoolExecutor);
}
执行结果:
任务提交成功 :0
开始执行:0
开始执行:1
任务提交成功 :1
任务提交成功 :2
任务提交成功 :3
任务提交成功 :4
当前线程池中的线程数量为:2
当前任务队列中的任务数量为:3
-------------------
执行结束:0
执行结束:1
开始执行:2
开始执行:3
执行结束:2
执行结束:3
开始执行:4
执行结束:4
当前线程池中的线程数量为:2
当前任务队列中的任务数量为:0
2.3 处理线程池的拒绝策略
// 预计结果:
// 1、 1个任务直接分配线程开始执行
// 2、 1个任务进入等待队列
// 3、 队列不够用,临时加开1个线程来执行任务(5秒没活干就销毁)
// 4、 队列和线程池都满了,剩下2个任务,没资源了,被拒绝执行。
// 5、 任务执行,10秒后,如果无任务可执行,销毁临时创建的1个线程
private void threadPoolExecutorTest() throws Exception {
// 创建一个 核心线程数量为1,最大数量为2,等待队列最大是1 的线程池,也就是最大容纳3个任务。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 2, 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(1), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("有任务被拒绝执行了");
}
});
testCommon(threadPoolExecutor);
}
执行结果
开始执行:0
任务提交成功 :0
任务提交成功 :1
任务提交成功 :2
开始执行:2
任务提交成功 :3
任务提交成功 :4
有任务被拒绝执行了
有任务被拒绝执行了
当前线程池中的线程数量为:2
当前任务队列中的任务数量为:1
-------------------
执行结束:0
执行结束:2
开始执行:1
执行结束:1
当前线程池中的线程数量为:2
当前任务队列中的任务数量为:0
2.4 Executors.newFixedThreadPool(int nThreads)
// 预计结果:线程池线程数量为:5,超出数量的任务,其他的进入队列中等待被执行
private void threadPoolExecutorTest() throws Exception {
// 和Executors.newFixedThreadPool(int nThreads)一样的
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
testCommon(threadPoolExecutor);
}
执行结果(同2.2):
任务提交成功 :0
开始执行:0
任务提交成功 :1
开始执行:1
任务提交成功 :2
任务提交成功 :3
任务提交成功 :4
当前线程池中的线程数量为:2
当前任务队列中的任务数量为:3
-------------------
执行结束:0
执行结束:1
开始执行:2
开始执行:3
执行结束:2
执行结束:3
开始执行:4
执行结束:4
当前线程池中的线程数量为:2
当前任务队列中的任务数量为:0
2.5 Executors.newCachedThreadPool()
// 预计结果:
// 1、 线程池线程数量为:5,超出数量的任务,其他的进入队列中等待被执行
// 2、 所有任务执行结束,10秒后,如果无任务可执行,所有线程全部被销毁,池的大小恢复为0
private void threadPoolExecutorTest() throws Exception {
// 和Executors.newCachedThreadPool()一样的
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 10L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
testCommon(threadPoolExecutor);
Thread.sleep(10000L);
System.out.println("10秒后,再看线程池中的数量:" + threadPoolExecutor.getPoolSize());
}
执行结果:
开始执行:0
任务提交成功 :0
任务提交成功 :1
开始执行:1
任务提交成功 :2
开始执行:2
开始执行:3
任务提交成功 :3
开始执行:4
任务提交成功 :4
当前线程池中的线程数量为:5
当前任务队列中的任务数量为:0
-------------------
执行结束:4
执行结束:2
执行结束:3
执行结束:1
执行结束:0
当前线程池中的线程数量为:5
当前任务队列中的任务数量为:0
10秒后,再看线程池中的数量:0
2.6 Executors.newScheduledThreadPool(int nThreads)
2.6.1 基础使用
private void threadPoolExecutorTest() throws Exception {
// 和Executors.newScheduledThreadPool()一样的
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
threadPoolExecutor.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务被执行,现在时间:" + System.currentTimeMillis());
}
}, 3000, TimeUnit.MILLISECONDS);
System.out.println(
"定时任务,提交成功,时间是:" + System.currentTimeMillis() + ", 当前线程池中线程数量:" + threadPoolExecutor.getPoolSize());
// 预计结果:任务在3秒后被执行一次
}
执行结果:
定时任务,提交成功,时间是:1584029368832, 当前线程池中线程数量:1
任务被执行,现在时间:1584029371833
2.6.2 scheduleAtFixedRate
private void threadPoolExecutorTest() throws Exception {
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
// 提交后,2秒后开始第一次执行,之后每间隔1秒,固定执行一次(如果发现上次执行还未完毕,则等待完毕,完毕后立刻执行)。也就是说这个代码中是,3秒钟执行一次(计算方式:每次执行三秒,间隔时间1秒,执行结束后马上开始下一次执行,无需等待)
threadPoolExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务-1 被执行,现在时间:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
}
2.6.3 scheduleAtFixedDelay
private void threadPoolExecutorTest() throws Exception {
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
// 提交后,2秒后开始第一次执行,之后每间隔1秒,固定执行一次(如果发现上次执行还未完毕,则等待完毕,等上一次执行完毕后再开始计时,等待1秒);也就是说这个代码钟的效果看到的是:4秒执行一次。 (计算方式:每次执行3秒,间隔时间1秒,执行完以后再等待1秒,所以是 3+1)
threadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务-2 被执行,现在时间:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
}
三、关于如何确定合适的线程数量
这里提供一种基础的思路,根据业务类型来确定:
- 计算型任务(占用的是CPU资源):需要线程执行大量计算的,消耗的大部分资源是CPU资源,可以设定为CPU核心数量的1~2倍
- IO型任务(占用的是硬盘、网络等资源):需要线程执行大量的读写等耗时操作而非大量的数值计算,则数值可以适当地放大一些,具体可以根据实际的IO阻塞时常来定(例如Tomcat的默认线程数是200)