说明:
本文主要探究任务提交到线程池,以及任务如何被执行,以及如何使用 Future 来获取任务执行的返回结果。
一、线程池的好处
1、线程池的重用
线程池的创建和销毁的开销巨大,通过线程池的重用大大减少了这些不必要的开销,既然减少了内存开销,其线程执行速度也回提高。
2、控制线程池的并发数
控制线程池的并发数可以有效的避免大量的线程池争夺CPU资源而造成堵塞。
3、线程池也可以对线程进行管理
线程池可以提供定时、定期、单线程、并发数控制等功能。比如通过SheduledThreadPool线程池来执行S秒后,每隔N秒执行一次的任务。
二、线程池使用方式
1、编写好线程池配置,并将执行器注入到容器
@Configuration
public class SourceThreadPoolConfig {
@Value("${srm-source.core-pool-size:4}")
private Integer core;
@Value("${srm-source.max-pool-size:10}")
private Integer maxPoolSize;
@Value("${srm-source.keep-alive-seconds:300}")
private Integer keepAliveSeconds;
@Value("${srm-source.queue-capacity:200}")
private Integer queueCapacity;
@Value("${srm-source.thread-name-prefix:srm-source-thread-execute}")
private String threadNamePrefix;
@Bean
@Qualifier("executor")
public ThreadPoolTaskExecutor executor() {
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(core);
// 设置最大线程数
executor.setMaxPoolSize(maxPoolSize);
// 除核心线程外的线程存活时间
executor.setKeepAliveSeconds(keepAliveSeconds);
// 如果传入值大于0,底层队列使用的是LinkedBlockingQueue,否则默认使用SynchronousQueue
executor.setQueueCapacity(queueCapacity);
// 线程名称前缀
executor.setThreadNamePrefix(threadNamePrefix);
// 设置拒绝策略。当线程池中没有线程的时候调用当前线程池的线程去执行任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
2、直接@Autowire注入即可使用
public class Demo{
// 直接注入线程池
@Autowired
@Qualifier("executor")
private ThreadPoolExecutor executor;
public void test(){
// 直接将任务提交到线程池,交给线程池去执行
executor.submit(()->{
// 具体业务逻辑
// xxxxx
});
}
}
三、线程提交的基础流程
我们要向线程池提交一个任务,我们一般都是使用 executor.submit(runnable / callable)
的方式提交任务到线程池中。首先需要明白线程池执行线程的基础流程,如下:
①、当一个任务提交到线程池以后,首先判断线程池中当前工作的线程数量有没有达到核心线程数量。
②、如果没有,则调用本地方法(native方法),向操作系统申请创建一个线程用于执行该任务。
③、如果有,则将该线程封装成一个node,然后添加到等待队列中(具体添加的逻辑还比较复杂)。
④、如果等待队列已经满了,则检查线程池中的线程数量是否已经达到了最大线程数量。
1、若已经达到了最大线程数量,则执行拒绝策略(可自己定义拒绝策略,也可以使用默认的拒绝策略)。
2、若没有,则继续开启线程去执行任务,直到达到最大线程数量
以 ThreadPoolExecutor.submit()
方法为例,进行如下探究!
四、任务提交到线程池
一般我们都是通过
ThreadPoolExecutor.submit()
的方式将任务提交到线程池!一个任务提交到线程池,大致分为三步:
1、将任务封装 为 FutureTask
2、执行 execute
3、返回
A)、封装为 FutureTask
1、submit方法
可以看到 submit 方法有三个重载方法,该方法可以提交 Runnable 和 Callable 类型的任务。不管是提交的 Runnable 还是 Callable 类型的任务,都会在该方法中被封装为一个 FutureTask 然后提交到线程池中。
public abstract class AbstractExecutorService implements ExecutorService {
// submit 重载方法1
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// ①、提交到线程池的时候会将 Runnable 任务封装为一个 FutureTask
RunnableFuture<Void> ftask = newTaskFor(task, null);
// ②、执行。后面详细介绍
execute(ftask);
// ③、返回 Future ,可通过 Future.get到线程执行后的返回值(这里的返回值就是 null )
return ftask;
}
// submit 重载方法2
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
// ①、提交到线程池的时候会将 Runnable 任务封装为一个 FutureTask
RunnableFuture<T> ftask = newTaskFor(task, result);
// ②、执行。后面详细介绍
execute(ftask);
// ③、返回 Future ,可通过 Future.get到线程执行后的返回值(这里的返回值就是传进来的 result )
return ftask;
}
// submit 重载方法3
public <T> Future<T> submit(Callable<T> task) {
if ①、(task == null) throw new NullPointerException();
// 提交到线程池的时候会将 Callable 任务封装为一个 FutureTask
RunnableFuture<T> ftask = newTaskFor(task);
// ②、执行。后面详细介绍
execute(ftask);
// ③、返回 Future ,可通过 Future.get到线程执行后的返回值
return ftask;
}
}
2、将 Callable 或 Runnable封装为一个FutureTask
不管是提交的 Runnable 还是 Callable 类型的任务,都会在该方法中被封装为一个 FutureTask!!!
①、newTaskFor
public abstract class AbstractExecutorService implements ExecutorService {
// 提交到线程池的时候会将 Callable 任务封装为一个 FutureTask
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
// 将 callable 封装为一个 FutureTask
return new FutureTask<T>(callable);
}
// 提交到线程池的时候会将 runnable 任务适配为 Callable 并封装为一个 FutureTask
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
// 将 Runnable 适配为 callable,并且将提交的Runnable任务封装为 FutureTask
return new FutureTask<T>(runnable, value);
}
}
②、具体封装为 FutureTask 的方法
可以看到 FutureTask 有两个构造函数,分别是对应处理
Runnable
和Callable
的。①、如果是runnable接口,则先将runnable接口适配为callable接口,然后再设置到 this.callable 变量上。
②、如果是
Callable
接口,则直接设置到 this.callable 变量上。
public class FutureTask<V> implements RunnableFuture<V> {
// callable接口并绑定到callable变量上
private Callable<V> callable;
// ①、处理 Runnable 接口
public FutureTask(Runnable runnable, V result) {
// 如果新建 FutureTask 对象的时候传入的是 runnable ,
// 则通过 Executors.callable(runnable, result)将 runnable 适配为 callable
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
// ②、处理 callable 接口
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
}
③、将Runnable适配为Callable
public class Executors {
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
// 将 Runnable 接口进行包装,适配为 callable
return new RunnableAdapter<T>(task, result);
}
/**
* 将 Runnable 接口进行包装,适配为 callable
*/
public static Callable<Object> callable(Runnable task) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<Object>(task, null);
}
/**
* A callable that runs given task and returns given result
* Runnable 接口适配器,实现了 Callable 接口。将 Runnable 适配为 Callable
*/
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
// 在call方法里调用 runnable的run方法。适配
task.run();
// 直接将传递过来的 result 返回给你,不做任何处理
return result;
}
}
}
B)、执行 execute()
执行流程分三步
在执行的线程数 < corePoolSize,新建线程。调用方法addWorker 自动检查运行状态和wokerCount数量。防止错误情况下增加线程,返回false。
如果一个任务成功增加到队列中,仍需要双重检测是否应该增加线程(因为存在上次检测后死掉的线程)或者线程池从上次进入这个方法时停止。所以需要再次检测状态,必要的时候回滚或者队列没有线程是要启动新的线程。
队列中无法增加新的任务,尝试新增一个线程,如果失败应该是停止或者队列饱和,所以拒绝这个任务。
下面的代码中的 addWorker 方法是重点方法,需要重点探究!!!
/**
* 据线程池的状态以及当前线程数(workerCount)判断是否需要新增一个worker,
* 还是直接放入等待队列,还是执行拒绝策略。
*/
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 【 1 】、worker数量比核心线程数小,直接创建worker执行任务
if (workerCountOf(c) < corePoolSize) {
// addWorker(command, true): 创建一个worker节点封装该任务,并执行该任务.该方法是重点方法
if (addWorker(command, true))
return;
c = ctl.get();
}
// 【 2 】、worker数量超过核心线程数,任务直接进入队列。
// workQueue.offer(command) 是添加到等待队列,如果添加成功了,还需要进行双重检查,检查是否该为该任务开启一个线程去执行他
// 条件成立: 线程池的状态是Running,并且放入到线程等待队列成功了
if (isRunning(c) && workQueue.offer(command)) {
// 获取线程池状态 和 工作线程数量
int recheck = ctl.get();
// 线程池状态不是RUNNING状态,说明执行过shutdown命令,需要对新加入的任务执行reject()操作。
// 这儿为什么需要recheck,是因为任务入队列前后,线程池的状态可能会发生变化。
if (! isRunning(recheck) && remove(command))
reject(command);
// 这儿为什么需要判断0值,主要是在线程池构造方法中,核心线程数允许为0
else if (workerCountOf(recheck) == 0)
// 如果检查通过,则开启一个线程来执行该任务
addWorker(null, false);
}
//【 3 】、 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务(即:线程阻塞队列满了但线程池中的线程数没达到最大线程数,
// 则新开启一个线程去执行该任务)。
// 这儿有3点需要注意:
// 1. 线程池不是运行状态时,addWorker内部会判断线程池状态
// 2. addWorker第2个参数表示是否创建核心线程
// 3. addWorker返回false,则说明任务执行失败,需要执行reject操作
else if (!addWorker(command, false))
// 拒绝该任务
reject(command);
}
C)、addWorker
将要执行的任务
FutureTask
包装成一个worker
节点,并且执行该任务(如果条件成立)。该方法会检查线程池状态,线程数量等信息,然后判断是否能够创建一个worker,如果判断通过,则创建一个worker并且向操作系统申请开启一个线程(或复用线程池中的线程)来执行任务!
/**
* firstTask : 表示要执行的任务
* core : 表示是否创建核心线程
*/
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
// 获取线程池状态
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
// 获取线程池中工作线程的数量
int wc = workerCountOf(c);
// 如果线程池中工作线程数量大于等于线程池容量 或者 大于等于corePoolSize
// 或 maximumPoolSize 则添加worker失败
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 将线程池中工作线程数量加一,然后跳出双重for循环
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
// 检查此时的线程池状态和第一次获取的线程池状态rs是否相等,若相等则继续for循环,
// 若不相等则跳出内层for循环重新获取
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// ===================上面只是检查了是否能创建节点,并没有真实创建=======================
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 将任务包装成一个worker节点。将任务 firstTask 封装到 Worker 对象的 firstTask 属性中
// 后面线程真正执行任务的时候其实调用的就是 Worker.firstTask.run()方法
w = new Worker(firstTask);
// 获取到 Worker 代表的线程
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
// 加锁
mainLock.lock();
try {
// 获取线程池运行状态
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
// isAlive() 是native方法
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// 添加到工作线程集合中
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
// 释放锁
mainLock.unlock();
}
// 如果worker成功的被添加到了线程池的正在工作线程集合,则直接调用 Thread.start() 运行任务
if (workerAdded) {
// =======================这步就是开启一个线程去执行任务=======================
//【 开启线程执行任务 】调用 Thread.start() 方法,向操作系统申请线程(或复用线程池中的线程)执行任务
// `t.start();`代码,该代码的含义就是调用native方法,向操作系统申请线程一个线程来执行任务(或复用线程池中的线程)。
t.start();
// 设置任务已被开启
workerStarted = true;
}
}
} finally {
// 如果开启线程失败,则将该worker节点从工作线程node的集合中移除
if (! workerStarted)
addWorkerFailed(w);
}
// 返回线程是否成功开启
return workerStarted;
}
五、线程池执行已提交的任务
①、当一个任务提交到线程池,首先会被封装成一个worker对象(不管是否立即执行该任务)
②、根据线程池中的线程数量,线程池状态等判断该任务(worker)是否被 立即执行 或 进入等待队列 或 拒绝。
- 如果一个任务被立即执行,则立即执行该worker
- 如果一个任务是被添加到等待队列中了,但此时线程池中有空闲线程了,便会从等待队列中取出该worker并执行。
- 如果一个任务被拒绝了,则不会被执行。
此时执行该任务的worker调用的其实就是
Worker
里的run方法。③、根据该任务的执行情况:1、正常执行 2、发生异常 将结果封装到
outcome
变量中!
1、worker和futuretask和callable / runnable 的关系
要先理解这三者之间的关系,后面才能理解为什么任务执行的时候先调用worker 的run方法,然后再调用的futuretask的run方法,最后再调用的 任务的run方法!
**说明:**我们提交给线程池的任务会先被封装为了 FutureTask
,然后被封装成一个 Worker
。所以在线程池执行我们提交的任务的时候一定也是执行的Worker
里的 FutureTask
的run方法。所以我们需要关注: Worker.run
和 FutureTask.run
!!!
2、Worker.run()
// 任务提交后会被封装为一个 worker节点
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
// 具体任务的引用会被记录到这个属性上(即将 FutureTask 对象封装到该属性上)
Runnable firstTask;
Worker(Runnable firstTask) {
// 将任务赋值给 firstTask 属性
this.firstTask = firstTask;
// 这儿是Worker的关键所在,使用了线程工厂创建了一个线程。传入的参数为当前worker
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker */
// 代理
// 【当线程执行任务的时候就会执行这个方法】
public void run() {
// 执行 worker的run 方法
runWorker(this);
}
// 执行任务
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
// 加锁执行任务
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted())
wt.interrupt();
try {
// 执行run方法之前
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 【 调用run方法运行任务。这里其实是调用的 futureTask 的 run 方法。 】
task.run();
} catch (Exception x) {
// 省略.....
} finally {
afterExecute(task, thrown);
}
} finally {
// help GC
task = null;
// 将已执行的任务数量++
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
}
3、FutureTask.run()
在上一步的
task.run();
代码中便会调用到 FutureTask.run() 方法!如下
public class FutureTask<V> implements RunnableFuture<V> {
// submit(callable / runnable) runnable接口使用适配器模式适配成了 callable
private Callable<V> callable;
// 任务执行的返回值会被保存到该变量中(比如: 我们的 Callable 类型的任务是可以接收返回值的)
// 任务正常执行情况下:outcome 保存了任务的执行返回结果,即callable.call的返回值
// 任务异常执行情况下:outcome 保存了任务执行时抛出的异常, 即 callable.call 方法抛出的异常
private Object outcome;
// 当前任务被线程执行期间,保存当前执行任务的线程对象引用
private volatile Thread runner;
private volatile WaitNode waiters;
// ============= 线程真正执行任务的时候会调用该方法 =============
public void run() {
// 条件一:若 state != NEW 成立,则说明当前 task 已经被执行过了,
// 或者已经被取消了,总之就是,非 NEW 状态的任务,线程就不处理了
// 条件二:!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()) 条件成立,则说明 cas 设置 runnerOffset失败,说明当前任务被其他线程抢占了。
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
// 执行到这里,说明当前状态一定是 NEW 状态,并且当前线程抢占 Task 也成功
try {
Callable<V> c = callable;
// c != nul 防止空指针异常
// state == NEW 防止外部线程取消掉当前任务
if (c != null && state == NEW) {
// 结果引用
V result;
// true 表示 callable.run 代码块执行成功,未抛出异常
// false 表示 callable.run 代码块执行失败,抛出异常
boolean ran;
try {
// 【 执行任务 】调用我们自己实现的callable接口的call,
// 或是经过适配后的runnable接口的call方法
result = c.call();
// 执行成功则修改 ran 状态为 true
ran = true;
} catch (Throwable ex) {
result = null;
// 执行失败则修改 ran 状态为 false
ran = false;
// 设置异常信息,将异常信息封装到 outcome 中
setException(ex);
}
// 如果 ran = true 则说明 c.call()代码正常执行结束了
if (ran)
// 设置线程执行返回结果到 outcome 属性中
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
}
六、外部线程获取任务的执行结果
当我们提交任务到线程池后,会返回一个 Future ,我们可以根据这个 Future 来获取任务执行的返回结果,执行状态 或 取消执行等!
// 提交到线程池
Future<?> future = executorService.submit(futureTask);
// 获取任务执行结果
Object result = future.get();
// 带超时时间的获取任务执行结果方法。和get()方法一样,只是多了一个超时时间
future.get(100,TimeUnit.MICROSECONDS);
// 如果任务还没有被执行,可以调用 cancel 方法取消该任务
future.cancel(true);
// 判断该任务是否已经被取消了
future.isCancelled();
1、外部线程获取任务返回值(get())
通过
future.get();
方法即可获取到任务执行的返回值。①、如果任务已经执行完毕并且有了返回值(有可能是正常执行完毕,有可能是发生异常),则直接返回结果。
②、如果当前任务还没有被执行 或 没有执行完毕!则调用
Future.get()
方法的线程会被封装为一个WaitNode
并且阻塞 (LockSupport.park(this)
),直到该任务执行完毕后被唤醒(也可能会被中断),然后取到返回值。
/**
* @throws CancellationException {@inheritDoc}
* 该方法是外部线程调用 get
* 可能会有 多个线程 等待着get这个任务执行的返回值
* 场景:多个线程等待当前任务执行完后的结果....
*/
public V get() throws InterruptedException, ExecutionException {
// 获取当前任务状态
int s = state;
// 如果 s <= COMPLETING ,则说明未执行、正在执行、正在完成(总之就是当前任务还没执行结束)
// 则外部想要获取到当前任务的返回结果的外部线程会被阻塞在这里
if (s <= COMPLETING)
// 不带超时的等待,返回当前任务的状态
s = awaitDone(false, 0L);
// 返回 callable.call 方法的执行结果
return report(s);
}
2、awaitDone方法,阻塞调用线程
该方法用于当外部线程想要获取任务的执行结果的时候,如果当前任务还没有执行完毕,则将外部线程封装为一个
WaitNode
加入到等待队列中。
/**
* Awaits completion or aborts on interrupt or timeout.
* 该方法的作用:
* 该方法用于当外部线程想要获取任务的执行结果的时候,如果当前任务还没有执行完毕,则将外部线程加入到等待队列中
*/
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
// 0 表示不带超时
final long deadline = timed ? System.nanoTime() + nanos : 0L;
// 引用当前线程,封装成 WaitNode 对象
WaitNode q = null;
// 表示当前线程WaitNode对象没有入队
boolean queued = false;
// 自旋
for (;;) {
// 如果 Thread.interrupted() 条件成立,则说明当前线程是被其他线程使用 中断这种方式 唤醒的
// interrupted()方法返回true后,会将Thread中的中断标记重置回false
if (Thread.interrupted()) {
// 将当前外部线程的node从等待队列中移除。
removeWaiter(q);
// 向外部调用这个方法的方法抛出中断异常
throw new InterruptedException();
}
// 加入当前线程是被其他线程使用 unpark(thread)的方式唤醒的,会正常自旋,走下面的逻辑
int s = state;
// ①、条件成立,则说明当前任务已经有了结果。可能是好结果也可能是异常.....
if (s > COMPLETING) {
// 条件成立,则说明已经为当前线程创建过node了,此时需要 q.thread = null (help GC)
if (q != null)
q.thread = null;
// 直接返回当前状态
return s;
}
// ②、当前任务接近完成,或接近失败状态。这里让当前线程再次释放cpu,进入到下一次cpu争抢....
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
// ③、第一次自旋进入这里,为外部调用get()方法获取结果的线程创建一个node,将线程封装为node
else if (q == null)
q = new WaitNode();
// ④、第二次自旋进入这里,如果该线程对应的node节点还没有入队,则使用cas的方式入队(waiters一直指向等待队列的头节点)
else if (!queued){
// 入队
q.next = waiters;
// 返回是否入队成功
// 这里 waiters 指向等待队列头节点,上一步已经将 q.next 指向了 waiters 了,这里是通过cas的方式将 waiters 和 q交换
// 使用cas的方式将 waiters 指向当前节点,如果成功了则返回true,如果失败了则说明可能其他线程已经先一步将waiters指向他的node了,需要重试
queued = UNSAFE.compareAndSwapObject(this, waitersOffset, waiters, q);
}
// ⑤、第三次自旋进入这里(如果是带等待时间的)
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
// 这里只将线程 park nanos 时间
LockSupport.parkNanos(this, nanos);
}
// ⑥、当前执行get操作的线程就会被park掉,线程状态就会变成 waitting 状态了,相当于休眠了.....
// 除非有其他的线程将你唤醒,或将当前线程中断。注意和上面一句代码对比
else
// 将线程park掉
LockSupport.park(this);
}
}