我们为什么要使用线程池
这就需要对比着常用的多线程来说了,每次执行任务都会创建新线程去执行任务,而且没办法做统一的管理,所以池化技术的演变就是为了解决这些问题,特点如下:
- 节省资源: 通过连接复用技术,使得不再每次就创建线程,很大程度的节省了系统的资源。
- 提高时效: 因为连接复用,不再创建连接,所以节省创建连接的过程。
- 统一管理: 通过一个空间完成对线程的管理,使得任务进行统一的分配,监控。
1、核心组件ThreadPoolExecutor详解
在这里我就不会磨磨唧唧的说一些类关系的事了,直接进入主题,如果涉及到我会在后文进行简单的阐述。
1.1 ThreadPoolExecutor中的核心参数
- 核心线程数corePoolSize: 任务队列未达到容量的时候的最大可以同时运行的线程数,同时可以指定核心线程是否回收,默认不回收。
- 最大线程数maximumPoolSize: 当任务队列达到最大值,线程池可以同时运行的最大线程数。
- 任务队列workQueue: 当运行线程数到达核心线程数后,那么新进来的任务将会放到队列中。
- 非核心线程存活时间keepAliveTime: 当线程没有执行任务的时候,不会立即销毁,而是等待这个时长时候销毁。
- 存活时长的单位unit: 存活时长的单位。
- 线程工厂threadFactory: 使用
Executor
创建线程池的时候使用。 - 饱和策略handle: 当线程池无法接纳新任务的时候执行的饱和策略。
1.2 为什么阿里巴巴规定不能使用 Executors 去创建线程池?
我们先介绍一下使用 Executors 创建的线程池常见的有哪几种类型。
- FixedThreadPool: 这个线程池的特点是核心线程数和最大线程数是同样的数目,然后任务队列是Integer.MAX_VALUE,会导致这个线程池中存在大量的任务堆积,导致 OOM
- SingleThreadPoolExecutor: 这个线程池的特点是核心线程数和最大线程数都是 1,然后队列是Integer.MAX_VALUE,一样会导致任务堆积,导致
OOM
。 - CachedThreadPool: 这个线程池的特点是没有核心线程,全是非核心线程,而且允许创建的非核心线程数是
Integer.MAX_VALUE
,直接 CPU 给你干没了。
1.3 从源码角度解析 ThreadPoolExecutor 的原理
在看执行的源码之前先看一些其他的方法和属性。
下面的和线程池状态相关的,也就代表着线程池能否正常工作
//这个是计数位 SIZE 是 32,也就是 Integer 占的位数,4 字节,COUNT_BITS=29
private static final int COUNT_BITS = Integer.SIZE - 3;
//这个值等于 1 向左移动 29 位再-1,那么就是 28 个 1
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
//运行中状态--高三位 101
private static final int RUNNING = -1 << COUNT_BITS;
//调用shutdown 方法之后变为这个状态,高三位为 000
private static final int SHUTDOWN = 0 << COUNT_BITS;
//调用shutdownNow 方法之后变为 STOP,高三位为 001
private static final int STOP = 1 << COUNT_BITS;
//当状态为SHUTDOWN/STOP时候要清空任务队列,清空完之后为这个状态,高三位 010
private static final int TIDYING = 2 << COUNT_BITS;
//最后就是这个结束态了,线程池完全关闭,高三位 011
private static final int TERMINATED = 3 << COUNT_BITS;
//所以能看出只有高三位为 101 的时候线程池才能接受任务,然后看下面这个
//有一个 ctlOf 方法,执行逻辑就是return rs | wc;就是保留 1,看看是什么状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
接着看 execute
方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//先取出来线程池的窗台
int c = ctl.get();
//这个workerCountOf就是获取工作线程数,也是用 c 去计算
//逻辑就不粘贴了,c 就是两个作用,高三位存储线程池状态,然后剩下 29 位存储工作线程数
//如果工作线程数小于核心线程数,那么就 addWorker,一会说这个
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果 addWorker 没成功,往下走
//如果线程池还是运行状态,并且能进入任务队列的话
if (isRunning(c) && workQueue.offer(command)) {
//双重检查一下
int recheck = ctl.get();
//如果线程池不是运行状态 那就从任务队列移除,然后执行饱和策略
if (! isRunning(recheck) && remove(command))
reject(command);
//如果工作线程数为 0,那么就 addWorker 一个 null
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//线面就是入队失败,那么就添加非核心线程
//如果没 addWorker 成功,那么就执行饱和策略
else if (!addWorker(command, false))
reject(command);
}
从上面就看出一个关键,就是用 ctl
去计算状态和工作线程数,然后使用 addWorker
方法。接着看
//方法入参就能看出两个关键,一个是线程任务,一个是是否是核心线程的标识,会看上面的代码
//当任务入队之后,如果工作线程数为 0,那么就传入 null,和 false,那就是非核心线程干活了
//你的任务都是在队列里面,所以不需要传入了。
private boolean addWorker(Runnable firstTask, boolean core) {
//这个关键字类似于 goto 语句,一般和循环混合使用,比如 continue retry,就代表着循环
//从指定位置开始,对多重循环来说比较方便
retry:
//直接来一个死循环,还是看我们上面的状态的那个描述,高三位我都标识出来了
for (int c = ctl.get();;) {
// runStateAtLeast这个方法就是判断 第一个参数是否大于等于第二个参数
//这个判断的作用是什么?我们可以看到除了 RUNNING,剩下的都是大于 0的
//而且代表着线程池不可用,所以下面的判断就是线程池的状态是不是不可用的
//如果不可用的话,那么就直接 return false
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP)
|| firstTask != null
|| workQueue.isEmpty()))
return false;
//否则继续死循环
for (;;) {
//判定一下线程数,如果指定是核心线程任务,那么就判断是否大于等于核心线程数
//否则就判断是否大于等于最大线程数,如果判定是 true,那么就说明这个任务
//不能正确执行,要么是核心线程没有空闲了,要么是线程超了
if (workerCountOf(c)
>= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
return false;
//如果能正确执行,怕么就比较并增长线程数,然后跳出循环往下走
if (compareAndIncrementWorkerCount(c))
break retry;
// 如果没有增长成功线程数,说明这个任务线程分配失败了,就一直比较增长
//如果线程池状态不对了,那么就从最顶层循环开始
c = ctl.get(); // Re-read ctl
if (runStateAtLeast(c, SHUTDOWN))
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//所以上面就是干了一件事,就是不断的查线程池状态,保证可用,然后增加线程数目,这时候
//没创建线程,就是增长数目,相当于占座了,只有成功才能往下走。
//把他想象成举手提问,然后最后一个人回答问题就好理解了。
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//将线程任务包装成 worker,里面包含了线程任务和一个新线程
w = new Worker(firstTask);
final Thread t = w.thread;
//如果线程部位 null
if (t != null) {
//这里加锁 ,然后将任务放入到 workers 里面,并维护最大线程数,】
//这里需要判断线程池状态等
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int c = ctl.get();
//这里就是如果线程池正常的情况下那么就进入 workers 等待调用
if (isRunning(c) ||
(runStateLessThan(c, STOP) && firstTask == null)) {
if (t.getState() != Thread.State.NEW)
throw new IllegalThreadStateException();
workers.add(w);
workerAdded = true;
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
}
} finally {
mainLock.unlock();
}
//如果 worker 成功添加,那么就异步线程执行了
if (workerAdded) {
//线程任务执行
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
上面的 addWorker
主要就是包装线程任务成为一个 Worker
,那么 Worker
是什么结构,我们看类的信息就足够了
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
它是一个实现了 Runnable
的类,同时继承了 AQS
,所以里面一定是有加锁,释放锁的逻辑的,同时里面的 run
方法就是执行线程任务的逻辑,它调用了一个 this.runWorker
,也就是交给 ThreadPoolExecutor
来执行,接着看。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
//这个 getTask 就是不断的从 workQueue 中拿任务,然后执行,
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
try {
task.run();
afterExecute(task, null);
} catch (Throwable ex) {
afterExecute(task, ex);
throw ex;
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
从这里再梳理一下,ThreadPoolExecutor
放任务的时候判断线程数是否大于核心线程,如果不大于,就包装成 Worker
然后 run
,否则就进阻塞队列,然后这些任务就一直执行,一直 getTask
,如果 get
不到阻塞呗,拿到了就执行,所以这就体现出了线程复用,小于核心线程的 Worker
不断的执行,执行完就去队列中拿。
还有一个非常关键的一点,就是非核心线程的事,我们大多数听到的执行情况都是先使用核心线程执行任务,然后当核心线程最大值之后就放入阻塞队列中,当阻塞队列满的时候,创建非核心线程执行任务。这个说法体现在了哪里呢?
这个核心线程和非核心线程其实都是理论上的知识,而在实际的源码中都是线程,只不过创建的时机是不同的,我们看上面的源码 addWorker
方法的 core
参数其实就是一个比较线程数和核心线程还是最大线程的标识,最后都是要 addWorker
的,所以并不是非核心线程执行完就销毁,而是同核心线程一样,不断的取队列的任务执行,干的活和核心线程一样,以此达到最大的并发效率。
这个只是简略的过程图,实际上有很多判定条件,比如线程池的状态,线程的数目等等,在源码的注释上面我都写了。
接下来我们来看一下ThreadPoolExecutor
中定义的几个饱和策略。
1.4 ThreadPoolExecutor 中的饱和策略
1. AbortPolicy: 抛出RejectedExecutionException
异常,拒绝新任务执行。
2. CallerRunsPolicy: 调用主线程(执行该任务的线程,不是线程池中的了)运行任务。
3. DiscardPolicy: 直接丢弃新任务
4. DiscardOldestPolicy: 丢弃最早未处理的任务请求
2、怎么选择合适的线程数量?
这个其实没有非常完美的公式进行推导计算,在现在的应用场景中大致分为两种
- CPU 密集型任务: 线程任务包含大量的计算逻辑。
- IO 密集型任务: 网络 IO,文件读写,数据库读写操作等。
对于 CPU
密集型任务,线程数设置过多会加重CPU
上下文切换的代价,所以推荐是核心数+1;
对于IO 密集型任务,线程数可以设置稍微多一些,毕竟对 CPU
的使用相较于前者还是很轻松的,推荐是核心数*2+1。
但是这只是理论上面的推荐,时间生产过程中没,是需要根据情况进行压测计算的,根据实际的结果来得到一个理想的线程数。而在实际的过程中我们可能还需要动态的调整线程池参数,所以给大家推荐一个开源框架,美团团队开源的 Dynamic-tp
,非常好用,下一篇文章将会实操使用它。