前言
线程池是一种基于池化思想管理线程的工具。随着多核CPU成为主流,多线程并行处理任务逐渐成为了开发小伙伴在工作中提升服务器处理任务能力的必备技能。
线程池简介
线程池的好处
降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁的开销
提高响应速度:线程池执行任务,不需要创建线程就可以立即执行
提高线程的可管理性:线程池可以统一分配、管理和监控,避免无限制的创建线程,消耗系统资源
线程池相关类介绍
线程池相关的类或接口有如下四个
![](https://i-blog.csdnimg.cn/blog_migrate/e5fe9413cb95831acc42a798c44f2dbb.png)
Executor
Executor接口仅提供一个执行提交可执行(Runnable)任务的方法
void execute(Runnable command);
ExecutorService
ExecutorService接口提供了shutdown相关方法和submit后者方法,前者用于关闭线程池,后可以提交包含返回值的任务。
// 有序关闭执行器,会执行之前已经提交的任务,但不接受新任务
void shutdown();
// 尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行任务的列表
List<Runnable> shutdownNow();
// 提交带返回值的任务
<T> Future<T> submit(Callable<T> task);
// 批量提交任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
// ...省略其他方法
AbstractExecutorService
AbstractExecutorService是ExecutorService的默认实现,它通过RunnableFuture实现了submit,invokeAny和invokeAll方法。
// 提交一个Runnable任务,并且返回Future对象
public Future<?> submit(Runnable task)
// 提交一个Callable任务,并返回Future对象
public <T> Future<T> submit(Callable<T> task)
// 提交一组任务,如果有任意一个执行完,则返回结果
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
// 提交一组任务,全部执行完成后返回Future的List
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
// 省略其他方法
ThreadPoolExecutor
ThreadPoolExecutor是线程池的具体实现,线程池中线程管理,任务管理,拒绝策略等都是在这个类中实现的
官方文档
An ExecutorService that executes each submitted task using one of possibly several pooled threads, normally configured using Executors factory methods.
ExecutorService是一个可以从多个池化线程中选择其中一个线程处理提交的任务,通常会利用Executors工厂方法进行配置。
Thread pools address two different problems: they usually provide improved performance when executing large numbers of asynchronous tasks, due to reduced per-task invocation overhead, and they provide a means of bounding and managing the resources, including threads, consumed when executing a collection of tasks. Each ThreadPoolExecutor also maintains some basic statistics, such as the number of completed tasks.
线程池解决了两个不同的问题:它们通常在执行大量异步任务时提供更好的性能,这是因为减少了每任务调用开销;它们提供了一种绑定和管理执行任务集合时消耗的资源(包括线程)的方法。每个ThreadPoolExecutor还维护一些基本统计信息,例如已完成任务的数量。
线程池参数介绍
创建一个线程池时需要以下几个参数
corePoolSize
核心线程数,当线程池中的线程数量小于核心线程数时,若提交一个任务到线程池,即使有其它空闲线程能够执行新任务也会创建一个新线程来执行任务。
maximumPoolSize
最大线程数,它是线程池可以创建线程的最大数量,如果队列满了,并且以创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。
keepAliveTime
存活时间,当线程空闲超过设置的存活时间,并且线程池中的线程数量超过核心线程数(corePoolSize),则线程会被回收,
TimeUnit
超时时间的单位
BlockingQueue<Runnable>
任务队列,它是用于保存等待执行任务的阻塞队列。
ThreadFactory
用于创建线程的工厂,可以通过线程工厂给线程创建更有意义的名字。
RejectedExecutionHandler
拒绝策略,当队列和线程池都满了,说明线程处于饱和状态,线程池会采取一种策略来处理新提交的任务,默认策略是AbortPolicy,当无法处理新任务时直接抛出异常。
JDK提供的4中拒绝策略
DiscardPolicy:不处理任务,直接丢弃
CallerRunsPolicy:使用调用者所在线程来执行任务
DiscardOldestPolicy:丢弃队列里最老的一个任务,并调用execute方法执行当前任务
AbortPolicy:直接抛出异常
钩子函数
线程池提供了两个可以重写的钩子函数beforeExecute(Thread, Runnable)和afterExecute(Runnable, Throwable)。这两个钩子函数分别会在任务执行之前和任务执行之后被调用。这两个函数通常可以用于初始化执行环境,添加日志,添加一些统计信息等。这两个方法需要通过继承线程池来重写
ThreadPoolExecutor任务处理过程
当一个新的任务提交到线程池时,线程池处理流程如图所示
判断线程池状态是否存活,如果不
todo 这里要补充内容
![](https://i-blog.csdnimg.cn/blog_migrate/ec067eecc23fe928f467705f6c52826a.png)
ThreadPoolExecutor使用样例
首先我们自定义一个线程池,重写ThreadPoolExecutor中的beforeExecutor方法和afterExecute方法,两个方法分别输出一段文字到控制台。
public class MyThreadPoolExecutor extends ThreadPoolExecutor {
// ...省略构造方法
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("任务开始执行啦~~~");
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("任务执行完成啦~~~");
}
}
自定义一个任务,实现Runnable接口,每个任务模拟执行100ms。
public static class MyTask implements Runnable {
private String name; // 任务名称
public MyTask(String name) {this.name = name;}
@Override
public void run() {
System.out.println(name + "执行中");
try { Thread.sleep(100); } catch (InterruptedException e) {throw new RuntimeException(e);}
}
}
创建一个核心线程数为1,最大线程数为1,任务队列容量为1的线程池。给这个线程池提交3个任务。
public static void main(String[] args) throws InterruptedException {
MyThreadPoolExecutor threadPoolExecutor = new MyThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1));
try{
threadPoolExecutor.execute(new MyTask("任务1"));
threadPoolExecutor.execute(new MyTask("任务2"));
threadPoolExecutor.execute(new MyTask("任务3"));
}finally {
threadPoolExecutor.shutdown();
}
}
上面main方法如果不用try方法包起来,进程不会终止,大家知道是什么原因吗?
执行后的结果如下所示,向线程池提交第一个任务时,线程池会创建一个线程执行此任务。向线程池提交第二个任务时,由于任务1还在执行中,并且核心线程数已经满了,因此线程池会将线程放到队列中。向线程池提交第三个任务时,线程池的核心线程数已满,任务队列已满,最大线程数已满,因此会执行默认的拒绝策略,抛出RejectedExecutionException异常。
![](https://i-blog.csdnimg.cn/blog_migrate/0f2bb08d89af31fda87f86b438d37188.png)
线程池使用注意事项
线程池源码分析
线程池容量与状态
在线程池中使用ctl变量来表示线程池工作线程数与线程池的状态,ctl是一个AtomicInteger。其中高三位表示线程池的状态,低29为表示线程的线程池当前工作线程数。
线程状态分析
我们拿RUNNING来举例分析,-1的二进制为1111 1111 1111 1111 1111 1111 1111 1111 左移29位后 1110 0000 0000 0000 0000 0000 0000 0000,取高三位的值位111。
线程池工作线程数分析
在线程池官方文档中或者Executors框架几种线程池创建的最大线程数为Integer.MAX_VALUE。通过上面的分析可以看出,实际线程池最大线程数不可能为Integer.MAX_VALUE。由于ctl高3位表示线程的状态,因此线程池最大线程数为2^29-1。
打包和拆包函数
为了方便计算ctl以及根据ctl快速获得线程池状态或者线程池当前工作线程数。Doug Lea大佬封装了ctl的打包和拆包函数
runStateOf
该方法可以根据ctl计算线程池状态
workerCountOf
该方法可以根据ctl计算线程池工作线程数
ctlOf
该方法可以将入参线程池状态和线程池工作线程数计算为ctl
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 线程池最大线程容量,COUNT_BITS=29
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 3个高位的二进制数表示线程池的状态
// 高3位:111
private static final int RUNNING = -1 << COUNT_BITS;
// 高3位:000
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 高3位:001
private static final int STOP = 1 << COUNT_BITS;
// 高3位:010
private static final int TIDYING = 2 << COUNT_BITS;
// 高3位:011
private static final int TERMINATED = 3 << COUNT_BITS;
// ctl拆包方法
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
Submit源码分析
我们使用线程池常用的方法submit其实是定义在ThreadPoolExecutor的父类上的,submit方法会将Runnable或者Callable包在一个FutureTask中,并传入execute
// AbstractExecutorService#submit
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task); // 创建了一个FutureTask
execute(ftask);
return ftask;
}
execute是线程池中最核心的方法,execute方法主要有三个步骤
如果当前线程池中的线程数量小于核心线程数,我们需要调用addWorker方法尝试创建新的线程来执行此任务,如果创建成功并执行成功,则返回。如果创建失败则刷新c,继续下面的步骤
如果一个任务可以成功排队,那么我们仍然需要再次检查是否应该添加线程(因为自上次检查以来已有线程已死亡),或者池在进入此方法后关闭。因此,我们重新检查状态,如果需要,如果停止,则回滚排队,如果没有,则启动新线程。
若我们无法对任务进行排队,那个么我们尝试添加一个新线程。如果它失败了,说明线程池已经关闭或饱和,因此拒绝该任务。
// ThreadPoolExecutor#execute
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) { // 判断线程数是否小于核心线程数,如果小于核心线程数,则创建线程并执行此任务
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) { // 如果线程池是运行中,尝试添加到队列中
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command)) // 如果线程池已经不再运行中,则拒绝此任务
reject(command);
else if (workerCountOf(recheck) == 0) // 如果线程池中工作线程数量为0,则尝试添加一个线程
addWorker(null, false); // 这里没有把任务传到参数中,是因为当前任务已经在队列中了,因此不需要作为第一个任务执行
}
else if (!addWorker(command, false)) // 尝试添加新的线程执行任务
reject(command);
}
Worker介绍
Worker继承了AbstractQueuedSynchronizer,实现了Runnable接口。
它继承AQS实现了一个独占锁,在执行shutdown,setCorePoolSize,setMaximumPoolSize等方法时会尝试中断,在中断方法中会先尝试获取锁,如果获取锁失败说明worker正在运行中
实现Runnable接口的主要目的时存储要执行的任务
private final class Worker extends AbstractQueuedSynchronizer implements Runnable
worker中的run方法
worker中的run方法代理到外部ThreadPoolExecutor中的runWorker的方法
// ThreadPoolExecutor.Worker#run
public void run() {
runWorker(this);
}
runWorker内部是一个死循环,不断从workQueue中获取任务
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 {
beforeExecute(wt, task); // 执行beforeExecute钩子函数
Throwable thrown = null;
try {
task.run(); // 执行task中的run()方法
} catch (...) {
// 省略catch方法
} finally {
afterExecute(task, thrown); // 执行afterExcute钩子函数
}
} finally {
task = null;
w.completedTasks++;
w.unlock(); // 解锁
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
addWorker源码分析
有了上面Worker类的介绍,下面再来分析addWorker方法就容易的多,addWorker方法核心主要是下面四件事
使用CAS增加线程数量
创建Worker类,并添加到线程池的workerSet中
启动worker中的线程
处理worker启动失败
// ThreadPoolExecutor#addWorker
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
// ...省略判断线程池状态的代码,如果如果线程池已经SHUTDOWN,则返回false
for (;;) {
// ...省略线程数量校验:判断线程是否大于线程容量或者大于核心该线程数/最大线程数
if (compareAndIncrementWorkerCount(c)) // 添加线程数量
break retry;
// ... 省略判断线程池状态
}
}
// ...省略部分代码。如果代码已经执行到这里,说明线程数量已经增加,可以创建线程了
try {
w = new Worker(firstTask); // 创建一个worker
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock; // 给worker的HashSet添加锁
mainLock.lock();
try {
//... 省略判断线程池和部分线程状态代码
workers.add(w);
//... 省略部分worker数量校验代码
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start(); // 启动线程
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w); // 线程走到这里说明线程启动失败,执行addWorkerFailed方法,将woker从set中移除,减少线程数并尝试关闭线程池
}
return workerStarted;
}