【Java并发编程】阿里最喜欢问的几道线程池的面试题?
引言
上一篇文章我们有介绍过线程池的一个基本执行流程《【Java并发编程】面试必备之线程池》以及它的7个核心参数,以及每个参数的作用、以及如何去使用线程池
还留了几个小问题。。建议看这篇文章之前可先看下前面那篇文章。这篇文章我们就来分析下上篇文章的几个小问题
- 线程池是否区分核心线程和非核心线程?
- 如何保证核心线程不被销毁?
- 线程池的线程是如何做到复用的?
我们先看最后一个问题一般一个线程执行完任务之后就结束了,Thread.start()
只能调用一次,一旦这个调用结束,则该线程就到了stop
状态,不能再次调用start
。如果你对一个已经启动的线程对象再调用一次start
方法的话,会产生:IllegalThreadStateException
异常,但是Thread
的run
方法是可以重复调用的。所以这里也会有一个面试经常问到的问题:Thread类中run()和start()方法的有什么区别?
下面我们就从jdk
的源码来一起看看如何实现线程复用的:
线程池执行任务的ThreadPoolExecutor
#execute
方法为入口
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 线程池当前线程数小于 corePoolSize 时进入if条件调用 addWorker 创建核心线程来执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 线程池当前线程数大于或等于 corePoolSize ,就将任务添加到 workQueue 中
if (isRunning(c) && workQueue.offer(command)) {
// 获取到当前线程的状态,赋值给 recheck ,是为了重新检查状态
int recheck = ctl.get();
// 如果 isRunning 返回 false ,那就 remove 掉这个任务,然后执行拒绝策略,也就是回滚重新排队
if (! isRunning(recheck) && remove(command))
reject(command);
// 线程池处于 running 状态,但是没有线程,那就创建线程执行任务
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果任务放入 workQueue 失败,则尝试通过创建非核心线程来执行任务
// 创建非核心线程失败,则说明线程池已经关闭或者已经饱和,会执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
excute方法主要业务逻辑
- 如果当前的线程池运行线程小于coreSize,则创建新线程来执行任务。
- 如果当前运行的线程等于coreSize或多余coreSize(动态修改了coreSize才会出现这种情况),把任务放到阻塞队列中。
- 如果队列已满无法将新加入的任务放进去的话,则需要创建新的线程来执行任务。
- 如果新创建线程已经达到了最大线程数,任务将会被拒绝。
addWorker 方法
上述方法的核心主要就是addWorker方法,
private boolean addWorker(Runnable firstTask, boolean core) {
// 前面还有一部分就省略了。。。。
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
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());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
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;
}
这个方法我们先看看这个work类吧
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
work类实现了Runnable接口,然后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 {
// 判断 task 是否为空,如果不为空直接执行
// 如果 task 为空,调用 getTask() 方法,从 workQueue 中取出新的 task 执行
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);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
这个runwork
方法中会优先取worker
绑定的任务,如果创建这个worker的时候没有给worker
绑定任务,worker
就会从队列里面获取任务来执行,执行完之后worker
并不会销毁,而是通过while
循环不停的执行getTask
方法从阻塞队列中获取任务调用task.run()来执行任务,这样的话就达到了线程复用的目的。 while (task != null || (task = getTask()) != null)
这个循环条件只要getTask
返回获取的值不为空这个循环就不会终止, 这样线程也就会一直在运行。
那么任务执行完怎么保证核心线程不销毁?非核心线程销毁?
答案就在这个getTask()
方法里面
private Runnable getTask() {
// 超时标记,默认为false,如果调用workQueue.poll()方法超时了,会标记为true
// 这个标记非常之重要,下面会说到
boolean timedOut = false;
for (;;) {
// 获取ctl变量值
int c = ctl.get();
int rs = runStateOf(c);
// 如果当前状态大于等于SHUTDOWN,并且workQueue中的任务为空或者状态大于等于STOP
// 则操作AQS减少工作线程数量,并且返回null,线程被回收
// 也说明假设状态为SHUTDOWN的情况下,如果workQueue不为空,那么线程池还是可以继续执行剩下的任务
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
// 操作AQS将线程池中的线程数量减一
decrementWorkerCount();
return null;
}
// 获取线程池中的有效线程数量
int wc = workerCountOf(c);
// 如果主动开启allowCoreThreadTimeOut,或者获取当前工作线程大于corePoolSize,那么该线程是可以被超时回收的
// allowCoreThreadTimeOut默认为false,即默认不允许核心线程超时回收
// 这里也说明了在核心线程以外的线程都为“临时”线程,随时会被线程池回收
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 这里说明了两点销毁线程的条件:
// 1.原则上线程池数量不可能大于maximumPoolSize,但可能会出现并发时操作了setMaximumPoolSize方法,如果此时将最大线程数量调少了,很可能会出现当前工作线程大于最大线程的情况,这时就需要线程超时回收,以维持线程池最大线程小于maximumPoolSize,
// 2.timed && timedOut 如果为true,表示当前操作需要进行超时控制,这里的timedOut为true,说明该线程已经从workQueue.poll()方法超时了
// 以上两点满足其一,都可以触发线程超时回收
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// 尝试用AQS将线程池线程数量减一
if (compareAndDecrementWorkerCount(c))
// 减一成功后返回null,线程被回收
return null;
// 否则循环重试
continue;
}
try {
// 如果timed为true,阻塞超时获取任务,否则阻塞获取任务
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
// 如果poll超时获取任务超时了, 将timeOut设置为true
// 继续循环执行,如果碰巧开发者开启了allowCoreThreadTimeOut,那么该线程就满足超时回收了
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
所以保证线程不被销毁的关键代码就是这一句代码
Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
只要timed
为false
这个workQueue.take()
就会一直阻塞,也就保证了线程不会被销毁。timed
的值又是通过allowCoreThreadTimeOut
和正在运行的线程数量是否大于coreSize
控制的。
- 只要
getTask
方法返回null
我们的线程就会被回收(runWorker
方法会调用processWorkerExit
) - 这个方法的源码也就解释了为什么我们在创建线程池的时候设置了
allowCoreThreadTimeOut
=true
的话,核心线程也会进行销毁。 - 通过这个方法我也们可以回答上面那个问题线程池是不区分核心线程和非核心线程的。
结束
- 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
- 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
- 感谢您的阅读,十分欢迎并感谢您的关注。
巨人的肩膀摘苹果
http://objcoding.com/2019/04/25/threadpool-running/
【Java并发编程】阿里最喜欢问的几道线程池的面试题?的更多相关文章
- Python并发编程之消息队列补充及如何创建线程池(六)
大家好,并发编程 进入第六篇. 在第四章,讲消息通信时,我们学到了Queue消息队列的一些基本使用.昨天我在准备如何创建线程池这一章节的时候,发现对Queue消息队列的讲解有一些遗漏的知识点,而这些知 ...
- 并发编程中死锁、递归锁、进程/线程池、协程TCP服务器并发等知识点
1.死锁 定义; 类似两个人分别被囚禁在两间房子里,A手上拿着的是B囚禁房间的钥匙,而B拿着A的钥匙,两个人都没法出去,没法给对方开锁,进而造成死锁现象.具体例子代码如下: # -*-coding:u ...
- 【Java并发编程】12、ThreadLocal 解决SimpleDateFormat非线程安全
大致意思:Tim Cull碰到一个SimpleDateFormat带来的严重的性能问题,该问题主要有SimpleDateFormat引发,创建一个 SimpleDateFormat实例的开销比较昂贵, ...
- Java 并发编程中的 CountDownLatch 锁用于多个线程同时开始运行或主线程等待子线程结束
Java 5 开始引入的 Concurrent 并发软件包里面的 CountDownLatch 其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是 ...
- java并发编程JUC第十一篇:如何在线程之间进行对等数据交换
java.util.concurrent.Exchanger可以用来进行数据交换,或者被称为"数据交换器".两个线程可以使用Exchanger交换数据,下图用来说明Exchange ...
- 【Java并发编程】:使用wait/notify/notifyAll实现线程间通信
在java中,可以通过配合调用Object对象的wait()方法和notify()方法或notifyAll()方法来实现线程间的通信.在线程中调用wait()方法,将阻塞等待其他线程的通知(其他线程调 ...
- Java并发编程的艺术(九)——批量获取多条线程的执行结果
当向线程池提交callable任务后,我们可能需要一次性获取所有返回结果,有三种处理方法. 方法一:自己维护返回结果 // 创建一个线程池 ExecutorService executorServic ...
- 《Java并发编程实战》读书笔记-第2章 线程安全性
要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问. 修复多线程问题的方式: 不在线程之间共享该状态变量 将状态变量修改为不可变的变量 在访问状态变量时使用同步 ...
- JUC 并发编程--09, 阻塞队列: DelayQueue, PriorityBlockingQueue ,SynchronousQueue, 定时任务线程池: ScheduledThreadPoolExecutor
先看DelayQueue 这个是用优先级队列实现的无界限的延迟队列,直接上代码: /** * 这个是 {@link DelayQueue} 延时队列 的验证使用类 */ class MyDelayed ...
随机推荐
- 【Linux】Linux 资料大全(100% 纯干货)
作者:李春港 出处:https://www.cnblogs.com/lcgbk/p/14071875.html 本文章给大家分享一些 Linux 学习资料,包含:社区网站.在线教程.命令大全.在线模拟 ...
- fist-第九天冲刺随笔
这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzzcxy/2018SE1 这个作业要求在哪里 https://edu.cnblogs.com/campus/fz ...
- 安装rose遇到的问题
将rose文件夹里面的rose.rar直接解压到rose安装目录里面的common文件夹中 安装教程与问题解决连接https://blog.csdn.net/Farewell_w/article/de ...
- PyQt(Python+Qt)学习随笔:QListView的layoutMode属性和batchSize属性
老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 batchSize属性 该属性是在layoutMode属性设置为Batched时,用于控制每个批量的 ...
- 网鼎杯2020 AreUSerialz
0x00 前言 ...有一说一,赵总的BUUCTF上的这道题目并没有复现到精髓.其实感觉出题人的题目本身没有那么简单的,只不过非预期实在是太简单惹. 涉及知识点: 1.php中protected变量反 ...
- 串口数据监视-Bus Hound
Bus Hound使用说明 一.打开该工具,会看到最上面的六个图标:1.Capture(捕捉按钮):按下它选择捕捉数据界面2.Save(保存按钮):按下它选择保存数据界面3.Setting(设置按钮) ...
- 矩阵乘法优化DP复习
前言 最近做毒瘤做多了--联赛难度的东西也该复习复习了. Warning:本文较长,难度分界线在"中场休息"部分,如果只想看普及难度的可以从第五部分直接到注意事项qwq 文中用(比 ...
- 【杂记】CSP-S 2020 游记 &反思
考场经历 吐槽:为什么这个 Enter 的位置怎么诡异啊老是打错.要是考挂了就怪你 开场当然先看了 T1,发现是个日期转换,果然是个百出不厌的模拟题类型,估价细节多到爆炸.看了几分钟题目,对这些规则云 ...
- Codeforces Edu Round 56 A-D
A. Dice Rolling 把\(x\)分解为\(a * 6 + b\),其中\(a\)是满6数,\(b\)满足\(1 <= b < 6\),即可... #include <io ...
- 自动化管理平台rundeck的安装方法
简介 RunDeck 是用 Java/Grails 写的开源工具,帮助用户在数据中心或者云环境中自动化各种操作和流程.通过命令行或者web界面,用户可以对任意数量的服务器进行操作,大大降低了对服务器自 ...