目录
1. 为什么需要线程池
如果我们有大量任务需要处理,频繁地创建和销毁线程会消耗时间和内存,为了让线程重复利用线程,让它们继续执行其他任务而不是立即销毁。
2. 线程池的核心线程数、最大线程数该如何设置
线程池中有两个非常重要的参数:
corePoolSize:核心线程数,表示线程池中的常驻线程的个数
maximumPoolSize:最大线程数,表示线程池中能开辟的最大线程个数
那这两个参数该如何设置呢? 我们根据线程池负责执行的任务分为以下几种情况:
CPU密集型任务:
CPU密集型任务的特点时,线程在执行任务时会一直利用CPU,所以对于这种情况,就尽可能避免发生线程上下文切换。比如,我们有两个CPU密集型任务,如果在一个线程内执行,那么任务的总时间则为每个任务执行时间之和;如果我们是每个任务分别被一个线程执行,则这两个任务总时间则为每个任务执行时间+上下文切换时间。所以对于CPU密集型任务,线程数最好就等于CPU核心数。
可以通过以下API拿到你电脑的核心数:
Runtime.getRuntime().availableProcessors()
只不过,为了应对线程执行过程发生缺页中断或其他异常导致线程阻塞的请求,我们可以额外在多设置一个线程,这样当某个线程暂时不需要CPU时,可以有替补线程来继续利用CPU。所以,对于CPU密集型任务,我们可以设置线程数为:CPU核心数+1
IO密集型任务:IO型任务,线程在执行IO型任务时,可能大部分时间都阻塞在IO上,假如现在有10个CPU,如果我们只设置了10个线程来执行IO型任务,那么很有可能这10个线程都阻塞在了IO上,这样 这10个CPU就都没活干了,所以,对于IO型任务,我们通常会设置线程数为: 2*CPU核心数。通常,如果IO型任务执行的时间越长,那么同时阻塞在IO上的线程就可能越多,我们就可以设置更多 的线程,但是,线程肯定不是越多越好。我们可以通过以下这个公式来进行计算:线程数 = CPU核心数 *( 1 + 线程等待时间 / 线程运行总时间 )线程等待时间:指的就是线程没有使用CPU的时间,比如阻塞在了IO线程运行总时间:指的是线程执行完某个任务的总时间我们可以利用jvisualvm抽样来估计这两个时间:![]()
总结:1. CPU密集型任务:CPU核心数+1,这样既能充分利用CPU,也不至于有太多的上下文切换成本2. IO型任务:建议压测,或者先用公式计算出一个理论值(理论值通常都比较小)3. 对于核心业务(访问频率高),可以把核心线程数设置为我们压测出来的结果,最大线程数可以等于核心线程 数,或者大一点点,比如我们压测时可能会发现500个线程最佳,但是600个线程时也还行,此时600就可以为最 大线程数4. 对于非核心业务(访问频率不高),核心线程数可以比较小,避免操作系统去维护不必要的线程,最大线程数可 以设置为我们计算或压测出来的结果。
3. 线程池的具体实现
3.1 线程池执行任务的具体流程
3. 2 线程池的五种状态的流转
线程池有五种状态:
- RUNNING:会接收新任务并且会处理队列中的任务
- SHUTDOWN:不会接收新任务并且会处理队列中的任务
- STOP:不会接收新任务并且不会处理队列中的任务,并且会中断在处理的任务(注意:一个任务能不能被中断 得看任务本身)
- TIDYING:所有任务都终止了,线程池中也没有线程了,这样线程池的状态就会转为TIDYING,一旦达到此状态,就会调用线程池的terminated()
- TERMINATED:terminated()执行完之后就会转变为TERMINATED
这五种状态并不能任意转换,只会有以下几种转换情况:
- RUNNING -> SHUTDOWN:手动调用shutdown()触发,或者线程池对象GC时会调用finalize()从而调用shutdown()。
- (RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()触发,如果先调shutdown()紧着调 shutdownNow(),就会发生SHUTDOWN -> STOP
- SHUTDOWN -> TIDYING:队列为空并且线程池中没有线程时自动转换
- STOP -> TIDYING:线程池中没有线程时自动转换(队列中可能还有任务)
- TIDYING -> TERMINATED:terminated()执行完后就会自动转换
3.3 线程池中的线程的关闭
线程池中的线程关闭的方式有:任务异常导致线程关闭;调用shutdown() 和shutdownNow()方法。
3.4 线程池为什么一定得是阻塞队列
![](https://i-blog.csdnimg.cn/blog_migrate/ebfe5db4796f85a8f8ebd5eebefb76e7.png)
3.5 线程发生异常,会被移出线程池吗?
会的,那有没有可能核心线程数在执行任务时都出错了,导致所有核心线程都被移出了线程
池?
在源码中,当执行任务时出现异常时,最终会执行processWorkerExit(),执行完这个方法后,当前线程也就自然消亡了,但是!processWorkerExit()方法中会额外再新增一个线程,这样就能维持住固定的核心线程数。
3.6 关键源码分析
3.6.1 基础属性
在线程池的源码中,会通过一个AtomicInteger类型的变量ctl,来表示线程池的状态和当前线程池中的工作线程数量。
其他的一些相关方法:
private static final int RUNNING = -1 << COUNT_BITS; private static final int SHUTDOWN = 0 << COUNT_BITS; private static final int STOP = 1 << COUNT_BITS; private static final int TIDYING = 2 << COUNT_BITS; private static final int TERMINATED = 3 << COUNT_BITS; // c状态是否小于s状态,比如RUNNING小于SHUTDOWN 7 private static boolean runStateLessThan(int c, int s) { return c < s; } // c状态是否大于等于s状态,比如STOP大于SHUTDOWN 12 private static boolean runStateAtLeast(int c, int s) { return c >= s; } // c状态是不是RUNNING,只有RUNNING是小于SHUTDOWN的 17 private static boolean isRunning(int c) { return c < SHUTDOWN; } // 通过cas来增加工作线程数量,直接对ctl进行加1 22 // 这个方法没考虑是否超过最大工作线程数的(2的29次方)限制,源码中在调用该方法之前会进行判断的 23 private boolean compareAndIncrementWorkerCount(int expect) { return ctl.compareAndSet(expect, expect + 1); } // 通过cas来减少工作线程数量,直接对ctl进行减1 28 private boolean compareAndDecrementWorkerCount(int expect) { return ctl.compareAndSet(expect, expect - 1); }
6.6.2 execute方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 获取ctl 6
// ctl初始值是ctlOf(RUNNING, 0),表示线程池处于运行中,工作线程数为0 7
int c = ctl.get();
// 工作线程数小于corePoolSize,则添加工作线程,并把command作为该线程要执行的任务 10
if (workerCountOf(c) < corePoolSize) {
// true表示添加的是核心工作线程,具体一点就是,在addWorker内部会判断当前工作线程数是不是超过了corePoolSize
// 如果超过了则会添加失败,addWorker返回false,表示不能直接开启新的线程来执行任务,而是应该先入队
if (addWorker(command, true))
return;
// 如果添加核心工作线程失败,那就重新获取ctl,可能是线程池状态被其他线程修改了
// 也可能是其他线程也在向线程池提交任务,导致核心工作线程已经超过了corePoolSize
c = ctl.get();
}
// 线程池状态是否还是RUNNING,如果是就把任务添加到阻塞队列中 22
if (isRunning(c) && workQueue.offer(command)) {
// 在任务入队时,线程池的状态可能也会发生改变 25
// 再次检查线程池的状态,如果线程池不是RUNNING了,那就不能再接受任务了,就得把任务从队列中移除,并进行拒绝策略
// 如果线程池的状态没有发生改变,仍然是RUNNING,那就不需要把任务从队列中移除掉
// 不过,为了确保刚刚入队的任务有线程会去处理它,需要判断一下工作线程数,如果为0,那就添加一个非核心的工作线程
// 添加的这个线程没有自己的任务,目的就是从队列中获取任务来执行 30
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果线程池状态不是RUNNING,或者线程池状态是RUNNING但是队列满了,则去添加一个非核心工作线程 37
// 实际上,addWorker中会判断线程池状态如果不是RUNNING,是不会添加工作线程的 38
// false表示非核心工作线程,作用是,在addWorker内部会判断当前工作线程数已经超过了maximumPoolSize,如果超过了则会添加不成功,执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
3.6.3 addWorker
对于addWorker方法,核心逻辑就是:
- 先判断工作线程数是否超过了限制
- 修改ctl,使得工作线程数+1
- 构造Work对象,并把它添加到workers集合中
- 启动Work对象对应的工作线程
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 线程池如果是SHUTDOWN状态并且队列非空则创建线程,如果队列为空则不创建线程了
// 线程池如果是STOP状态则直接不创建线程了
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
// 判断工作线程数是否超过了限制
// 如果超过限制了,则return false
// 如果没有超过限制,则修改ctl,增加工作线程数,cas成功则退出外层retry循环,去创建新的工作线程
// 如果cas失败,则表示有其他线程也在提交任务,也在增加工作线程数,此时重新获取ctl
// 如果发现线程池的状态发生了变化,则继续回到retry,重新判断线程池的状态是不是SHUTDOWN或STOP
// 如果状态没有变化,则继续利用cas来增加工作线程数,直到cas成功
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// ctl修改成功,也就是工作线程数+1成功
// 接下来就要开启一个新的工作线程了
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// Worker实现了Runnable接口
// 在构造一个Worker对象时,就会利用ThreadFactory新建一个线程
// Worker对象有两个属性:
// Runnable firstTask:表示Worker待执行的第一个任务,第二个任务会从阻塞队列中获取
// Thread thread:表示Worker对应的线程,就是这个线程来获取队列中的任务并执行的
w = new Worker(firstTask);
// 拿出线程对象,还没有start
final Thread t = w.thread;
if (t != null) {
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 rs = runStateOf(ctl.get());
// 如果线程池的状态是RUNNING
// 或者线程池的状态变成了SHUTDOWN,但是当前线程没有自己的第一个任务,那就表示当前调用addWorker方法是为了从队列中获取任务来执行
// 正常情况下线程池的状态如果是SHUTDOWN,是不能创建新的工作线程的,但是队列中如果有任务,那就是上面说的特例情况
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 如果Worker对象对应的线程已经在运行了,那就有问题,直接抛异常
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// workers用来记录当前线程池中工作线程,调用线程池的shutdown方法时会遍历worker对象中断对应线程
workers.add(w);
// largestPoolSize用来跟踪线程池在运行过程中工作线程数的峰值
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 运行线程
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
注意:在线程池中有这么一个参数: allowCoreThreadTimeOut ,表示是否允许核心工作线程超 时,意思就是是否允许核心工作线程回收 ,默认这个参数为false,但是我们可以调用allowCoreThreadTimeOut(boolean value)来把这个参数改为true,只要改了,那么核心工作线程也 就会被回收了,那这样线程池中的所有工作线程都可能被回收掉,那如果所有工作线程都被回收掉之 后,阻塞队列中来了一个任务,这样就形成了特例情况。