目录
工作线程本质就是一个Worker对象。再究其底层,其实就是thread.start启动的线程。
addWorker()源码剖析 (线程池如何添加工作线程?)
runWorker()源码剖析 (工作线程如何执行的任务?)
- 为什么要有线程池?
- Java是实现和管理线程池有哪些方式? 请简单举例如何使用。
- 为什么很多公司不允许使用Executors去创建线程池? 那么推荐怎么使用呢?
- ThreadPoolExecutor有哪些核心的配置参数? 请简要说明
- ThreadPoolExecutor可以创建哪三种线程池呢?
- 当队列满了并且worker的数量达到maxSize的时候,会怎么样?
- 说说ThreadPoolExecutor有哪些RejectedExecutionHandler策略? 默认是什么策略?
- 简要说下线程池的任务执行机制? execute –> addWorker (增加工作线程)–>runworker (getTask) (工作线程执行任务)
- 线程池中任务是如何提交的?
- 线程池中任务是如何关闭的?
- 在配置线程池的时候需要考虑哪些配置因素?
- 如何监控线程池的状态?
为什么要有线程池
减少性能开销:如果每次执行异步任务,都去构建一个全新的线程执行,但是任务执行完毕后,线程还要销毁。可以利用线程池,实现资源复用,线程处理完任务,不会立即销毁,而是等待新任务的到来。类似连接池。
可以充分发挥CPU性能:如果每次来任务都直接new,没有办法去把控线程的个数。如果太多了,上下文频繁的切换,性能有损耗。如果太少了,CPU资源没有发挥出来。线程池可以指定好工作线程的个数,充分发挥CPU资源。
监控的效果:如果直接new的话,无法查看线程的个数或者工作的状态。但是采用了线程池之后,本身帮咱们记录很多信息,工作线程个数,有多少个任务在排队,每一个工作线程处理了多少个任务,整个线程池处理了多少个任务
ThreadPoolExecutor应用方式
手动创建ThreadPoolExecutor
1、只需要构建好ThreadPoolExecutor对象即可,传入指定的7个参数。
2、在执行Runnable任务时,可以直接调用execute方法执行。
3、在执行Callable任务时,需要有返回结果,直接调用submit方法执行。
线程池的核心参数
工作线程本质就是一个Worker对象。再究其底层,其实就是thread.start启动的线程。
线程池的执行流程 (excute()方法的执行过程)
线程池的状态
线程池中的核心属性ctl
解释一个AtomicInteger是个什么鬼?
1、可以简单的看成,AtomicInteger就是一个int数值。
2、因为线程池中存在并发修改ctl的情况。AtomicInteger内部是基于CAS的方式,对ctl属性做修改。
Ps:CAS其实就是比较和交换,Compare And Swap。基于CPU支持的原语,来保证的原子性。保证修改某一个属性的时候,是原子性的。
ctl在线程池中,维护这两个信息:
- 工作线程的个数。
- 线程池的状态。
一个int类型,怎么维护这两个信息?线程池内部,将这个本质是int类型的ctl,拆分为32个bit位。
线程池的状态变化
线程池的状态
线程池中常用的两个函数
状态转化
线程池需要执行任务时,需要始终保证running状态
shutdown()
shutdownNow()
tryTerminate() 内部调用
terminated()
重写该方法,线程池销毁之后需要执行什么操作
excute()源码分析 (线程池的执行流程)
线程池中的工作线程就是一个个的work对象
工作线程本质就是一个Worker对象。再究其底层,其实就是thread.start启动的线程。
// 任务投递给线程池之后的处理
// command:投递过来的任务
public void execute(Runnable command) {
// 如果投递的任务为null,直接甩异常。
if (command == null)
throw new NullPointerException();
// 拿到核心属性ctl。
int c = ctl.get();
// workerCountOf:在获取工作线程个数
// 现在的工作线程数 小于 核心线程数
if (workerCountOf(c) < corePoolSize) {
// 基于addWorker方法,创建工作线程处理任务command,传递的true,代表构建核心线程
// addWorker,返回true,创建工作线程成功,反之失败。
if (addWorker(command, true))
// 代表创建线程成功,任务已经交给线程池了。return结束了。
return;
// 代表创建线程失败了,有情况,重新获取ctl。做后续判断
c = ctl.get();
}
// 任务交给核心线程处理失败了,代码走到这。
// 线程池状态是RUNNING么?如果是,就尝试将任务扔到工作队列
// 如果任务成功的投递到了工作队列排队,返回true,反之返回false
// 返回true,进到if,execute方法结束
if (isRunning(c) && workQueue.offer(command)) {
// 代码到这,说明任务已经添加到工作队列
// 再次获取ctl
int recheck = ctl.get();
// 再次判断,线程池状态是不是RUNNING
// 如果状态不是RUNNING,走remove方法,将任务从工作队列中移除。
// 移除任务操作,成功返回true,反之false
if (! isRunning(recheck) && remove(command))
// 说明任务移除成功,给任务甩一波拒绝策略。
reject(command);
// 状态是RUNNING,或者,没拒绝
// 查看工作线程个数是不是0个。
else if (workerCountOf(recheck) == 0)
// 创建一个非核心线程,处理这个任务,毕竟任务饥饿。
addWorker(null, false);
}
// 到这,说明任务没扔到工作队列。
// 创建非核心线程,处理任务。
else if (!addWorker(command, false))
// 如果创建非核心线程失败了,执行拒绝策略
reject(command);
}
addWorker()源码剖析 (线程池如何添加工作线程?)
1、判断线程池状态是否对劲
2、判断工作线程个数是否对劲
3、对ctl + 1
4、new Worker对象,扔到HashSet里。
5、启动Worker对象里的Thread。就执行了Worker里的run方法。
6、如果启动失败,要做补偿操作。
// 添加工作线程。
// firstTask,是任务
// core,true=核心,false=非核心
private boolean addWorker(Runnable firstTask, boolean core) {
// =======================做校验,线程池状态,工作线程个数====================================
retry:
for (;;) {
// 线程池状态
// 获取ctl。
int c = ctl.get();
// 基于ctl获取线程池状态
int rs = runStateOf(c);
// rs >= SHUTDOWN,如果满足,代表状态不是RUNNING。不是RUNNING不能接收新任务
if (rs >= SHUTDOWN &&
// 线程池状态是SHUTDOWN,没有传递任务,并且工作队列不为空
// 满足上述要求,代表当前要构建的工作线程,是要处理工作队列中的任务的。
// 如果工作队列有任务,但是没有工作线程在处理。任务饥饿。就会做addWorker(null,false)
!(rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
// 到这,代表当前状态不对劲,不能构建工作线程
return false;
for (;;) {
// 工作线程个数
// 到这说明状态ok。
// 拿到工作线程个数。
int wc = workerCountOf(c);
// CAPACITY:占用了29个bit位的数值,代表线程池能支撑的最大工作线程个数
// wc >= CAPACITY:如果满足,代表不能再创建工作线程了,超过最大值了。
if (wc >= CAPACITY ||
// 根据core判断,是否超过了核心线程数,或者最大线程数。
wc >= (core ? corePoolSize : maximumPoolSize))
// 工作线程数,达到阈值了,进到if,直接结束。
return false;
// 状态没问题,个数没问题。
// 对ctl + 1,如果成功,返回true,如果失败,返回false
if (compareAndIncrementWorkerCount(c))
// ctl + 1成功,跳出外层循环,准备走添加工作线程,并启动工作线程
break retry;
// 到这,CAS失败了,其他线程把ctl + 1了,ctl肯定改变了。
// 重新获取ctl
c = ctl.get();
// 线程池状态改变了么?
if (runStateOf(c) != rs)
// 跳出一次外层for循环
continue retry;
// 到这,说明状态没变,走内层for循环,判断工作线程个数就可以了。
}
}
// =======================添加工作线程,并启动工作线程====================================
// 两个标识,默认都是false
boolean workerStarted = false;
boolean workerAdded = false;
// Worker就是工作线程,先声明出来。
Worker w = null;
try {
// new 一个工作线程。会基于咱们自己提供的线程工厂,构建出Thread
w = new Worker(firstTask);
// 拿到线程工作构建的thread
final Thread t = w.thread;
// thread对象不是null,继续往下走。
if (t != null) {
// 拿线程池里的lock锁,加锁。 目的是为了确保HashSet操作的线程安全。
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 获取线程池的状态
int rs = runStateOf(ctl.get());
// 如果是RUNNING,一切正常,接着往下走
if (rs == RUNNING ||
// 解决任务饥饿的情况,代表正常,接着往下走
(rs == SHUTDOWN && firstTask == null)) {
// 一个Thread对象,连续两次执行start方法,会发生什么?
// 如果你的线程工厂,构建的Thread已经start了,直接扔你异常。
if (t.isAlive())
throw new IllegalThreadStateException();
// Worker对象扔到HashSet集合里。
workers.add(w);
// 获取工作线程个数
int s = workers.size();
// largestPoolSize记录当前线程池中工作线程个数的最大值记录
if (s > largestPoolSize)
// 如果现在的工作线程数,大于之前的记录,覆盖一下。
largestPoolSize = s;
// 代表工作线程添加成功。
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 添加成功了咩?
if (workerAdded) {
// 成功就,启动线程
t.start();
// 代表工作线程启动成功。
workerStarted = true;
}
}
} finally {
// 如果到这 workerStarted是false,代表工作线程启动失败。
if (! workerStarted)
// ctl + 1, workers.add(w)
// 从workers里移除worker,然后ctl - 1
addWorkerFailed(w);
}
// 返回结果。
return workerStarted;
}
工作线程的本质是什么?
工作线程本质就是一个Worker对象。再究其底层,其实就是thread.start启动的线程。
// 省略了部分代码
// Worker实现的Runnable,Worker重新了run方法,run方法就是任务
private final class Worker implements Runnable{
// thread对象
final Thread thread;
// 传递过来的第一个任务
Runnable firstTask;
// 当前工作线程,每完成一个任务,就+1
volatile long completedTasks;
// new Worker用的有参构造
Worker(Runnable firstTask) {
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
// Worker任务。thread对象执行了start方法走,走的是这个run方法。
public void run() {
// 工作线程执行任务,走的就是这个方法、
runWorker(this);
}
}
runWorker()源码剖析 (工作线程如何执行的任务?)
当线程启动后,会基于Worker对象里的run方法中的runWorker方法执行任务。
1、优先执行Worker携带的firstTask。
2、任务执行前后,会有勾子函数。
3、任务就是基于task.run执行的。
4、如果线程要凉凉,会执行补偿操作,ctl - 1,记录任务完成数,将worker从workers中移除。
// 省略了部分代码。
// 代码走的这,代表已经是另外一个线程了!!!!!这个线程是thread.start启动的。
// 已经是线程池里的工作线程在干活了!!!
final void runWorker(Worker w) {
// 拿到当前工作线程。
Thread wt = Thread.currentThread();
// task是第一个要执行的任务
Runnable task = w.firstTask;
// 把worker里的firstTask置为空 help gc
w.firstTask = null;
// 一个怪怪的属性!
boolean completedAbruptly = true;
try {
// 第一次循环,如果firstTask不为null,先执行firstTask
// 第二次循环,task必然为null,就需要走getTask再次获取新的任务执行。
while (task != null || (task = getTask()) != null) {
try {
// 任务前的勾子函数
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 执行任务
task.run();
} catch (Throwable x) {
thrown = x;
throw new Error(x);
} finally {
// 任务后的勾子函数
afterExecute(task, thrown);
}
} finally {
// 任务设置为null
task = null;
// 当前工作线程处理的任务个数 + 1
w.completedTasks++;
}
}
// 如果执行任务出现了异常,这行代码走么?
completedAbruptly = false;
} finally {
// 如果任务出现异常,需要走这个方法。
// 工作线程凉凉之前,做的处理
processWorkerExit(w, completedAbruptly);
}
}
Ps:如果工作线程在执行任务时,异常抛出来了,这个工作线程会怎么样?
如果task.run将异常抛出来了,会抛到runWorker方法,runWorker方法会将异常抛给run方法。
run方法异常结束。run方法结束,线程凉凉。
如果任务是基于FutureTask执行的,异常会被FutureTask捕获,线程就不会结束。
// 工作线程凉凉之前,做的操作
private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 如果没对ctl - 1,在这做个-1操作。
if (completedAbruptly)
decrementWorkerCount();
// 加锁,记录当前工作线程干了多少活,同时将worker对象从Workers中移除。
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}
}
getTask()源码剖析 (工作线程如何获取新任务?)
如果工作线程完成了firstTask,后续的任务,就需要基于getTask从工作队列中获取。
1、判断线程池状态是否正常。
2、判断工作线程个数,但是第一次循环进不去
3、在工作队列的位置,尝试获取任务,可能是poll,可能是take。
4、如果拿到任务,回到runWorker执行
5、如果没拿到任务,重新走for循环,在判断工作线程个数时,可能就会进入到if中,结束当前线程。
// getTask方法获取新任务执行。
private Runnable getTask() {
// timeOut,刚开始肯定是false。
boolean timedOut = false;
// 死循环。
for (;;) {
// 这两行看不懂面壁。。
int c = ctl.get();
int rs = runStateOf(c);
// 判断线程池状态
// 状态是SHUTDOWN,并且队列没任务。
if (rs == SHUTDOWN && workQueue.isEmpty())
// 状态是STOP,啥也不管,直接破产。
if (rs >= STOP)
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
// ctl - 1,工作线程准备凉凉……
decrementWorkerCount();
// 返回null,代表没任务了。
return null;
}
// 判断工作线程个数问题
// 拿到工作线程个数
int wc = workerCountOf(c);
// allowCoreThreadTimeOut:核心线程是否允许超时,默认是false,如果为true,
基于最大空闲时间走
// wc > corePoolSize:如果满足,代表现在有非核心线程在。
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// wc > maximumPoolSize:不会满足,理解就是一个健壮性判断,妹啥用。。。
// timed可以在第一次循环时为true,但是timeOut必然是false。第一次循环进不来,先不看!
// 第二次循环,timeOut可能为true了,第一个判断为null
if ((wc > maximumPoolSize || (timed && timedOut))
// wc > 1,至少是两个,即便工作有任务,也不会出现任务饥饿问题。
// workQueue.isEmpty(),工作队列为空,肯定没有任务饥饿问题。
&& (wc > 1 || workQueue.isEmpty())) {
// CAS的方式,将ctl - 1,只要-1成功,直接返回null
if (compareAndDecrementWorkerCount(c))
return null;
// 如果CAS失败,说明有并发,重新走for循环。
continue;
}
try {
// timed为true,走工作队列的poll方法,拿任务,等keepAliveTime时间,超过了,返回null
// timed为false,都是核心线程,执行take方法,死等,没任务就在这死等。等到有任务
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
// 如果r不为null,拿到任务了,回到runWorker继续执行任务
if (r != null)
return r;
// 到这,说明等了一会,没拿到任务,先把timedOut设置为true
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
线程池参数到底如何设置?
核心线程数,这个是最重要的。
第一个维度,要考虑这个线程池处理的任务类型是什么,是IO密集还是CPU密集。
CPU密集:需要CPU一直分配给这个线程时间片,程序一直执行。
IO密集:查询数据库,查询三方服务,涉及到磁盘IO或者网络IO比较多的情况。
第二个维度,需要考虑你服务器硬件的情况,CPU是多少核心的。
如果任务是CPU密集,核心线程数,设置到跟CPU核心数差不多即可。±1差不多。
如果任务是IO密集,IO密集的情况可能不太一样的,有的是去Redis查询,挺快,有的是去MySQL查询,有点慢,IO密集情况不用,核心线程数都会受到影响。核心线程数需要设置的比较多。
阻塞队列
阻塞队列和一般的队列在设计上有一个重要的区别:
阻塞队列能够在特定条件下阻塞操作的线程,如当队列满时阻塞生产者线程,或当队列空时阻塞消费者线程。这种阻塞机制使得阻塞队列在多线程环境中更为适合,特别是在线程池的任务调度中,阻塞队列可以保证线程间的协调和同步,从而避免任务丢失或争抢资源导致的错误。
-
当队列为空时:如果有线程尝试从队列中取元素,阻塞队列会阻塞该线程,直到有其他线程向队列中放入新的元素,才会解除阻塞,允许线程继续操作。
-
当队列已满时:如果有线程尝试向队列中放入元素,阻塞队列会阻塞该线程,直到队列中有空位(例如消费者线程消费了队列中的元素),然后解除阻塞并允许放入元素。