线程池原理与实战

Java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作:

  1. 必须为线程堆栈分配和初始化大量内存块,其中包含至少1MB的栈内存。
  2. 需要进行系统调用,以便在OS(操作系统)中创建和注册本地线程。

Java高并发应用频繁创建和销毁线程的操作是非常低效的,而且是不被编程规范所允许的。如何降低Java线程的创建成本?必须使用到线程池。线程池主要解决了以下两个问题:

  1. 提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度地对已经创建的线程进行复用,使得性能提升明显。
  2. 线程管理:每个Java线程池会保持一些基本的线程统计信息,例如完成的任务数量、空闲时间等,以便对线程进行有效管理,使得能对所接收到的异步任务进行高效调度。

说明在主要大厂的编程规范中,不允许在应用中自行显式地创建线程,线程必须通过线程池提供。由于创建和销毁线程需要时间以及系统资源开销,使用线程池的好处是减少这些开销,解决资源不足的问题。

1. JUC的线程池架构

在多线程编程中,任务都是一些抽象且离散的工作单元,而线程是使任务异步执行的基本机制。随着应用的扩张,线程和任务管理也变得非常复杂。为了简化这些复杂的线程管理模式,我们需要一个“管理者”来统一管理线程及任务分配,这就是线程池。在JUC中有关线程池的类与接口的架构图大致如图1-15所示。

image-20220412151555241

JUC就是java.util.concurrent工具包的简称,该工具包是从JDK 1.5开始加入JDK的,是用于完成高并发、处理多线程的一个工具包。

1. Executor

Executor是Java异步目标任务的“执行者”接口,其目标是执行目标任务。“执行者”Executor提供了execute()接口来执行已提交的Runnable执行目标实例。Executor作为执行者的角色,其目的是提供一种将“任务提交者”与“任务执行者”分离开来的机制。它只包含一个函数式方法:

image-20220412151627270

2.ExecutorService

ExecutorService继承于Executor。它是Java异步目标任务的“执行者服务接”口,对外提供异步任务的接收服务。ExecutorService提供了“接收异步任务并转交给执行者”的方法,如submit系列方法、invoke系列方法等,具体如下:

image-20220412151655074

3.AbstractExecutorService

AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。AbstractExecutorService存在的目的是为ExecutorService中的接口提供默认实现。

4.ThreadPoolExecutor

ThreadPoolExecutor就是大名鼎鼎的“线程池”实现类,它继承于AbstractExecutorService抽象类。

ThreadPoolExecutor是JUC线程池的核心实现类。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。

5.ScheduledExecutorService

ScheduledExecutorService是一个接口,它继承于ExecutorService。它是一个可以完成“延时”和“周期性”任务的调度线程池接口,其功能和Timer/TimerTask类似。

6.ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承于ThreadPoolExecutor,它提供了ScheduledExecutorService线程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。

ScheduledThreadPoolExecutor类似于Timer,但是在高并发程序中,ScheduledThreadPoolExecutor的性能要优于Timer。

7.Executors

Executors是一个静态工厂类,它通过静态工厂方法返回ExecutorService、ScheduledExecutorService等线程池示例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。

2. 线程池的标准创建方式

大部分企业的开发规范都会禁止使用快捷线程池(具体原因稍后介绍),要求通过标准构造器ThreadPoolExecutor去构造工作线程池。Executors工厂类中创建线程池的快捷工厂方法实际上是调用ThreadPoolExecutor(定时任务使用ScheduledThreadPoolExecutor)线程池的构造方法完成的。ThreadPoolExecutor构造方法有多个重载版本,其中一个比较重要的构造器如下:

image-20220412152952770

1.核心和最大线程数量

参数corePoolSize用于设置核心(Core)线程池数量,参数maximumPoolSize用于设置最大线程数量。线程池执行器将会根据corePoolSize和maximumPoolSize自动维护线程池中的工作线程,大致规则为:

  1. 当在线程池接收到新任务,并且当前工作线程数少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到corePoolSize。
  2. 如果当前工作线程数多于corePoolSize数量,但小于maximumPoolSize数量,那么仅当任务排队队列已满时才会创建新线程。通过设置corePoolSize和maximumPoolSize相同,可以创建一个固定大小的线程池。
  3. 当maximumPoolSize被设置为无界值(如Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务。
  4. corePoolSize和maximumPoolSize不仅能在线程池构造时设置,也可以调用setCorePoolSize()和setMaximumPoolSize()两个方法进行动态更改。

2.BlockingQueue

BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,如果线程池的核心线程都在忙,那么所接收到的目标任务缓存在阻塞队列中。

3.keepAliveTime

线程构造器的keepAliveTime(空闲线程存活时间)参数用于设置池内线程最大Idle(空闲)时长(或者说保活时长),如果超过这个时间,默认情况下Idle、非Core线程会被回收。

如果池在使用过程中提交任务的频率变高,也可以调用方法setKeepAliveTime(long,TimeUnit)进行线程存活时间的动态调整,可以将时长延长。如果需要防止Idle线程被终止,可以将Idle时间设置为无限大,具体如下:image-20220412153031415

默认情况下,Idle超时策略仅适用于存在超过corePoolSize线程的情况。但若调用了allowCoreThreadTimeOut(boolean)方法,并且传入了参数true,则keepAliveTime参数所设置的Idle超时策略也将被应用于核心线程。

3.向线程池提交任务的两种方式

向线程池提交任务的两种方式大致如下:

1
2
3
4
5
6
7
// Executor 接口中的方法
void execute(Runnable command) // 调用execute()方法

// ExecutorService 接口中的方法
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Feture<?> submit(Runable task);

以上的submit()和execute()两类方法的区别在哪里呢?大致有以下三点:

  1. 二者所接收的参数不一样

    Execute()方法只能接收Runnable类型的参数,而submit()方法可以接收Callable、Runnable两种类型的参数。

    Callable类型的任务是可以返回执行结果的,而Runnable类型的任务不可以返回执行结果。Callable是JDK 1.5加入的执行目标接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。

    Runnable和Callable的主要区别为:Callable允许有返回值,Runnable不允许有返回值;Runnable不允许抛出异常,Callable允许抛出异常。

  2. submit()提交任务后会有返回值,而execute()没有

    execute()方法主要用于启动任务的执行,而任务的执行结果和可能的异常调用者并不关心。submit()方法也用于启动任务的执行,但是启动之后会返回Future对象,代表一个异步执行实例,可以通过该异步执行实例去获取结果。

  3. submit()方便Exception处理

    execute()方法在启动任务执行后,任务执行过程中可能发生的异常调用者并不关心。而通过submit()方法返回的Future对象(异步执行实例),可以进行异步执行过程中的异常捕获。

1.通过submit()返回的Future对象获取结果

submit()方法自身并不会传递结果,而是返回一个Future异步执行实例,处理过程的结果被包装到Future实例中,调用者可以通过Future.get()方法获取异步执行的结果。通过submit()返回的Future对象获取异步执行结果,演示代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//测试用例:获取异步调用的结果
@Test
public void testSubmit2() {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
Future<Integer> future = pool.schedule(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
//返回200 - 300 之间的随机数
return RandomUtil.randInRange(200, 300);
}
}, 100, TimeUnit.MILLISECONDS);

try {
Integer result = future.get();
Print.tco("异步执行的结果是:" + result);
} catch (InterruptedException e) {
Print.tco("异步调用被中断");
e.printStackTrace();
} catch (ExecutionException e) {
Print.tco("异步调用过程中,发生了异常");
e.printStackTrace();
}
sleepSeconds(10);
//关闭线程池
pool.shutdown();

}

2.通过submit()返回的Future对象捕获异常

submit()方法自身并不会传递异常,处理过程中的异常都被包装到Future实例中,调用者在调用Future.get()方法获取执行结果时,可以捕获异步执行过程中抛出的受检异常和运行时异常,并进行对应的业务处理。演示代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//测试用例:提交和执行
@Test
public void testSubmit() {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.execute(new TargetTaskWithError());
/**
* submit(Runnable x) 返回一个future。可以用这个future来判断任务是否成功完成。请看下面:
*/
Future future = pool.submit(new TargetTaskWithError());

try {
//如果异常抛出,会在调用Future.get()时传递给调用者
if (future.get() == null) {
//如果Future的返回为null,任务完成
Print.tco("任务完成");
}
} catch (Exception e) {
Print.tco(e.getCause().getMessage());
}


sleepSeconds(10);
//关闭线程池
pool.shutdown();
}

在ThreadPoolExecutor类的实现中,内部核心的任务提交方法是execute()方法,虽然用户程序通过submit()也可以提交任务,但是实际上submit()方法中最终调用的还是execute()方法。

. Executors的4种快捷创建线程池的方法

image-20220412152108616

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
package com.crazymakercircle.mutithread.basic.create3;
import com.crazymakercircle.util.Print;
import com.crazymakercircle.util.RandomUtil;
import org.junit.Test;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import static com.crazymakercircle.util.ThreadUtil.sleepMilliSeconds;
import static com.crazymakercircle.util.ThreadUtil.sleepSeconds;

/**
* Created by 尼恩@疯狂创客圈.
*/

public class CreateThreadPoolDemo {

public static final int SLEEP_GAP = 500;
public static final int MAX_TURN = 5;

//异步的执行目标类
public static class TargetTask implements Runnable {
static AtomicInteger taskNo = new AtomicInteger(1);
protected String taskName;

public TargetTask() {
taskName = "task-" + taskNo.get();
taskNo.incrementAndGet();
}

public void run() {

Print.tco("任务:" + taskName + " doing");
// 线程睡眠一会
sleepMilliSeconds(SLEEP_GAP);

Print.tco(taskName + " 运行结束.");
}

@Override
public String toString() {
return "TargetTask{" + taskName + '}';
}
}

//异步的执行目标类:执行过程中将发生异常
static class TargetTaskWithError extends TargetTask {
public void run() {
super.run();
throw new RuntimeException("Error from " + taskName);
}
}

//测试用例:只有一条线程的线程池
@Test
public void testSingleThreadExecutor() {
//该方法用于创建一个“单线程化线程池”,也就是只有一个线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(如FIFO)执行。
ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
pool.execute(new TargetTask());
pool.submit(new TargetTask());
}
sleepSeconds(1000);
//关闭线程池
pool.shutdown();
//调用shutdown()方法来关闭线程池。执行shutdown()方法后,线程池状态变为SHUTDOWN,此时线程池将拒绝新任务,不能再往线程池中添加新任务,否则会抛出RejectedExecutionException异常。此时,线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成才会退出。
// 还有一个与shutdown()类似的方法,叫作shutdownNow(),执行shutdownNow()方法后,线程池状态会立刻变成STOP,并试图停止所有正在执行的线程,并且不再处理还在阻塞队列中等待的任务,会返回那些未执行的任务。
}
//(1)单线程化的线程池中的任务是按照提交的次序顺序执行的。
//(2)池中的唯一线程的存活时间是无限的。
//(3)当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的。
// 总体来说,单线程化的线程池所适用的场景是:任务按照提交次序,一个任务一个任务地逐个执行的场景。

//测试用例:只有3条线程固定大小的线程池
@Test
public void testNewFixedThreadPool() {
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
pool.execute(new TargetTask());
pool.submit(new TargetTask());
}
sleepSeconds(1000);
//关闭线程池
pool.shutdown();
}
// “固定数量的线程池”的特点大致如下:
//(1)如果线程数没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。
//(2)线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
//(3)在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。
// “固定数量的线程池”的适用场景:需要任务长期执行的场景。
// “固定数量的线程池”的线程数能够比较稳定地保证一个数,能够避免频繁回收线程和创建线程,故适用于处理CPU密集型的任务,在CPU被工作线程长时间占用的情况下,能确保尽可能少地分配线程。
// “固定数量的线程池”的弊端:内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。

//测试用例:“可缓存线程池”
//如果线程池内的某些线程无事可干成为空闲线程,“可缓存线程池”可灵活回收这些空闲线程。
@Test
public void testNewCacheThreadPool() {
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
pool.execute(new TargetTask());
pool.submit(new TargetTask());
}
sleepSeconds(1000);
//关闭线程池
pool.shutdown();
}
//“可缓存线程池”的特点大致如下:
//(1)在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。
//(2)此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
//(3)如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程。
//“可缓存线程池”的适用场景:需要快速处理突发性强、耗时较短的任务场景,如Netty的NIO处理场景、REST API接口的瞬时削峰场景。“可缓存线程池”的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。
//“可缓存线程池”的弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽。


//创建一个“可调度线程池”,即一个提供“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。Executors提供了多个创建“可调度线程池”的工厂方法:
//测试用例:“可调度线程池”
@Test
public void testNewScheduledThreadPool() {
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
for (int i = 0; i < 2; i++) {
scheduled.scheduleAtFixedRate(new TargetTask(),
0, 500, TimeUnit.MILLISECONDS);
//以上的参数中:
// 0表示首次执行任务的延迟时间,500表示每次执行任务的间隔时间
//TimeUnit.MILLISECONDS所设置的时间的计时单位为毫秒
}
sleepSeconds(1000);
//关闭线程池
scheduled.shutdown();
}


//测试用例:“可调度线程池2”
@Test
public void testNewScheduledThreadPool2() {
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(0);
for (int i = 0; i < 2; i++) {
scheduled.scheduleAtFixedRate(new TargetTask(),
0, 500, TimeUnit.MILLISECONDS);
//以上的参数中:
// 0表示首次执行任务的延迟时间,500表示每次执行任务的间隔时间
//TimeUnit.MILLISECONDS所设置的时间的计时单位为毫秒
}
sleepSeconds(1000);
//关闭线程池
scheduled.shutdown();
}


@org.junit.Test
public void testThreadPoolExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, //corePoolSize
100, //maximumPoolSize
100, //keepAliveTime
TimeUnit.SECONDS, //unit
new LinkedBlockingDeque<>(100));//workQueue

for (int i = 0; i < 5; i++) {
final int taskIndex = i;
executor.execute(() ->
{
Print.tco("taskIndex = " + taskIndex);
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
while (true) {
Print.tco("- activeCount:" + executor.getActiveCount() +
" - taskCount:" + executor.getTaskCount());
sleepSeconds(1);
}
}

//一个简单的线程工厂
static public class SimpleThreadFactory implements ThreadFactory {
static AtomicInteger threadNo = new AtomicInteger(1);

//实现其唯一的创建线程方法
@Override
public Thread newThread(Runnable target) {
String threadName = "simpleThread-" + threadNo.get();
Print.tco("创建一条线程,名称为:" + threadName);
threadNo.incrementAndGet();
//设置线程名称
Thread thread = new Thread(target, threadName);
//设置为守护线程
thread.setDaemon(true);
return thread;
}
}


@org.junit.Test
public void testThreadFactory() {
//使用自定义线程工厂,快捷创建线程池
ExecutorService pool =
Executors.newFixedThreadPool(2, new SimpleThreadFactory());
for (int i = 0; i < 5; i++) {
pool.submit(new TargetTask());
}
//等待10秒
sleepSeconds(10);
Print.tco("关闭线程池");
pool.shutdown();
}

//自定义拒绝策略
public static class CustomIgnorePolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 可做日志记录等
Print.tco(r + " rejected; " + " - getTaskCount: " + e.getTaskCount());
}
}

@org.junit.Test
public void testCustomIgnorePolicy() {
int corePoolSize = 2; //核心线程数
int maximumPoolSize = 4; //最大线程数
long keepAliveTime = 10;
TimeUnit unit = TimeUnit.SECONDS;
//最大排队任务数
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
//线程工厂
ThreadFactory threadFactory = new SimpleThreadFactory();
//拒绝和异常策略
RejectedExecutionHandler policy = new CustomIgnorePolicy();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime, unit,
workQueue,
threadFactory,
policy);

// 预启动所有核心线程
pool.prestartAllCoreThreads();
for (int i = 1; i <= 10; i++) {
pool.execute(new TargetTask());
}
//等待10秒
sleepSeconds(10);
Print.tco("关闭线程池");
pool.shutdown();
}

//线程本地变量,用于记录线程异步任务的开始执行时间
private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>();

@org.junit.Test
public void testHooks() {
ExecutorService pool = new ThreadPoolExecutor(2,
4, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(2)) {
@Override
protected void terminated() {
Print.tco("调度器已经终止!");
}

@Override
protected void beforeExecute(Thread t, Runnable target) {
Print.tco(target + "前钩子被执行");
//记录开始执行时间
START_TIME.set(System.currentTimeMillis());
super.beforeExecute(t, target);
}


@Override
protected void afterExecute(Runnable target, Throwable t) {
super.afterExecute(target, t);
//计算执行时长
long time = (System.currentTimeMillis() - START_TIME.get());
Print.tco(target + " 后钩子被执行, 任务执行时长(ms):" + time);
//清空本地变量
START_TIME.remove();
}
};


pool.execute(new TargetTask());

//等待10秒
sleepSeconds(10);
Print.tco("关闭线程池");
pool.shutdown();

}


@org.junit.Test
public void testNewFixedThreadPool2() {
//创建一个固定大小线程池
ExecutorService fixedExecutorService = Executors.newFixedThreadPool(1);
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) fixedExecutorService;
Print.tco(threadPoolExecutor.getMaximumPoolSize());
//设置核心线程数
threadPoolExecutor.setCorePoolSize(8);

//创建一个单线程化的线程池
ExecutorService singleExecutorService = Executors.newSingleThreadExecutor();
//转换成普通线程池, 会抛出运行时异常 java.lang.ClassCastException
((ThreadPoolExecutor) singleExecutorService).setCorePoolSize(8);
}





//测试用例:优雅关闭
@Test
public void testShutdownGracefully() {
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
threadPool.shutdown(); // Disable new tasks from being submitted
try {
// 设定最大重试次数
// 等待 60 s
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
// 调用 shutdownNow 取消正在执行的任务
threadPool.shutdownNow();
// 再次等待 60 s,如果还未结束,可以再次尝试,或则直接放弃
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("线程池任务未正常执行结束");
}
}
} catch (InterruptedException ie) {
// 重新调用 shutdownNow
threadPool.shutdownNow();
}
}


}