注:本文基于jdk17,在jdk17中对juc包进行了优化,在原功能不变的情况下可读性更好
以下代码是线程池的使用示例,这个示例中展示了两种使用线程池提交任务的方式
1、调用execute方法,该方法无返回值
2、调用submit方法,该方法会返回一个Future对象,通过其get方法能拿到任务的返回值
在下边的案例中,第一个execute案例大概率会报错,因为println方法底层有synchronized关键字,虽然是异步提交任务但是实际上执行时是串行,会触发拒绝策略
package mutilthread;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPoolTest {
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
public static void main(String[] args) throws ExecutionException, InterruptedException {
for (int i = 0; i < 100; i++) {
// threadPool.execute(new Task());
Future<Integer> count = threadPool.submit(new Task1());
System.out.println(count.get());
}
}
private static class Task implements Runnable {
private static AtomicInteger count = new AtomicInteger();
@Override
public void run() {
System.out.println(count.getAndIncrement());
}
}
private static class Task1 implements Callable<Integer> {
private static AtomicInteger count = new AtomicInteger();
@Override
public Integer call() throws Exception {
return count.getAndIncrement();
}
}
}
一、设计原则
线程池是按照尽可能少地创建线程的原则来进行调度,因此当线程数满足核心线程数后不会优先创建非核心线程,而是先向队列中放。只有当队列放不下了才会申请新的非核心线程。其大致流程是(execute方法的执行流程):
- 首先会判断当前线程数是否小于当前核心线程数,如果小于则创建线程来执行任务
- 如果当前线程数大于核心线程数,线程池会将任务加入队列
- 如果队列也满了,接着判断线程数是否小于最大线程数,如果当前线程数小于最大线程数则创建非核心线程来执行任务
- 如果队列满了并且也达到了最大线程数,则执行拒绝策略
二、参数
1、corePoolSize:核心线程数
2、maximumPoolSize:最大线程数
3、keepAliveTime:非核心线程的存活时间
4、TimeUnit:存活时间单位
5、workQueue:工作队列
6、threadFactory:创建线程的工厂
7、handler:拒绝策略,共有四种拒绝策略
- AbortPolicy:抛异常
- DiscardPolicy:不做任何处理
- DiscardOldestPolicy:丢弃旧任务,执行最新的任务
- CallerRunsPolicy:由任务提交线程执行,后面会附一个由该策略导致的生产bug
三、任务执行流程
在讲解执行流程之前先要讲解线程池中的ctl属性
1、ctl
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
线程池中通过一个AtomicInteger类型的ctl属性来表示当前线程池的状态和线程个数,其中
-
高3位用来表示线程池状态,注意转换成十进制的值,大于0的状态都是非正常运行状态
状态名称 ctl高3位 接收新任务 是否处理队列中的任务 状态说明 RUNNING 111-十进制7 是 是 正常运行 SHUTDOWN 000-十进制0 否 是 不接受新任务,但会处理已有任务,通过调用shutdown方法触发 STOP 001-十进制1 否 否 不接受新任务,抛弃已有任务,通过shutdownNow触发 TIDYING 010-十进制2 – – 任务执行完毕,活动线程数为0 TERMINATED 011-十进制3 – – 终结状态 -
其余29位用来表示线程池中的线程数量
2、执行流程
本节主要对第一节中提交任务部分做详细解释,提交的任务都通过addWorker方法执行的,在addWorker方法中会构造一个Worker对象,我们可以看一下类的定义,其实现了Runnable获取了异步执行能力,继承了AbstractQueuedSynchronizer获取了同步功能。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{}
addWorker方法最终会执行t.start(),此时会在新的线程中执行Worker对象的run方法,进而执行runWorker方法
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 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);
}
}
以上方法就是线程池的核心,该方法会通过一个while循环,先执行创建Woker对象时提交的任务,然后从任务队列中取任务,当队列中没有任务时该线程阻塞。
关于线程存活时间的实现是通过从队列中取任务的getTask方法实现的,线程池默认支持非核心线程的超时也支持核心线程的超时
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
// Check if queue empty only if necessary.
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
以上方法中会通过allowCoreThreadTimeOut来判断是否支持核心线程超时,可以通过allowCoreThreadTimeOut方法设置,当不支持核心线程超时的时候,通过wc > corePoolSize,也就是当前工作线程数是否大于核心线程数来判断是否需要开启非核心线程的超时。当支持超时的时候,会调用阻塞队列的poll方法并设置等待时间,超过等待时间最终会返回null值。null值被上层方法runWorker接收后就会跳出循环,执行finally中的processWorkerExit方法,该方法中会将Worker对象从线程池中移除并销毁Worker对象。如果支持核心线程超时的话,最终会将线程池里的Woker对象清空。
四、拒绝策略导致的bug
这是一个我所在的某大厂生产环境出现的一个故障
现象:某C端互动游戏,通过长连接进行rpc调用,在0点时突然出现rpc耗时极高但cpu和内存均无明显异常,内部使用的类dubbo的rpc服务持续告警线程池耗尽。导致长连接与rpc服务之间的tcp通道阻塞,buffer均被打满。
排查:通过链路分析工具发现该rpc接口的tps和逻辑耗时都比压测时要高,对代码进行分析发现在该接口中使用了一个2个核心线程,4个最大线程,队列为10的线程池执行数据入库操作,其拒绝策略为提交线程执行
分析:由于0点活动导致大量用户参与,tps瞬间激增数倍,由于数据入库属于io操作,耗时较长,导致线程池被瞬间打满而执行了拒绝策略。恰好拒绝策略用的是提交线程执行,而rpc服务的线程池也承受不了这么大的并发压力,导致rpc服务的线程池被瞬间打满,其拒绝策略是抛异常,因此收到大量告警邮件。由此导致tpc的接收缓冲区被打满,长连接的发送缓冲区被打满,进而导致了整个服务的不可用。
修复:修改线程池线程数,修改拒绝策略后恢复
总结:jdk中提供的4中拒绝策略要慎重使用,针对不同的场景一定要考虑好拒绝策略带来的影响,必要时可以自定义拒绝策略。
五、关于阻塞队列的选择
可选任务队列如下:
队列名称 | 并发控制 | 数量 | 阻塞 | 性能 |
---|---|---|---|---|
LinkedBlockingQueue | 支持 | 支持 | 支持 | 高 |
ArrayBlockingQueue | 支持 | 支持 | 支持 | 中 |
SynchronousQueue | 支持 | 不支持 | 支持 | 低 |
LinkedBlockingDeque | 支持 | 支持 | 支持 | 中 |
LinkedBlockingQueue和ArrayBlockingQueue由于其实现原理,ArrayBlockingQueue向队列中获取元素、向队列中放元素的时候都获取的是同一把锁,因此涉及频繁队列操作的场景不适合使用ArrayBlockingQueue,但是其遍历性能要比LinkedBlockingQueue高。
如果使用SynchronousQueue一定要做好测试,SynchronousQueue本身是个无缓冲阻塞队列,只能容纳一个元素,很容易触发拒绝策略。
LinkedBlockingDeque是一个双向队列,只是比LinkedBlockingQueue多支持了一些双端操作,在线程池这个场景下没有什么特定的应用场景
总结一下:如果在没有对使用场景做充分的分析和测试的情况下建议首选LinkedBlockingQueue,SynchronousQueue在任务场景下都要慎重使用!