【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 ...
随机推荐
- PyQt(Python+Qt)学习随笔:QScrollBar以及QAbstractSlider滚动条部件功能详解
专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 一.概述 在Designer输入部件中Horizo ...
- 转:浅析windows下字符集和文件编码存储/utf8/gbk
最近老猿在学习文件操作及网络爬虫相关知识,发现字符集及编码的处理非常重要,而老猿原来对此了解并不多,因此找了几篇文章看了一下,将老猿认为比较的相关文章转载一下.感谢各位原创大神! 1,字符集 这里主要 ...
- Fiddle过滤目标主机
测试某管理系统,查看接口的调用: 点击Actions->Run Filterset now,即可过滤出设置的域名. 若使用通配符*,可将含域名的一级二级域名过滤出.
- metasploit魔鬼训练营靶机环境搭建(第二章)
环境搭建,书上已经很详细了,路由转发的那个鼓捣了好久都没弄好,菜的啊 所以先往书后面继续学习,不停留在配置环境上了. backtrack没有下载,使用的kali linux 其他的都是一样的 百度网盘 ...
- 团队项目6——Alpha阶段项目复审
复审团队 广东靓仔六强选手 复审员 钟俊豪(3118005122) 复审内容 小组名称和链接 优点 缺点&Bug报告 最终排名 代码敲不队https://www.cnblogs.com/pip ...
- Day3 【Scrum 冲刺博客】
每日会议总结 昨天已完成的工作 方晓莹(PIPIYing) 开始人员管理页 搭建与后台对接的相关配置 方子茵(Laa-L) 完成车辆查询接口 黄芯悦(Sheaxx) 完善社区通知页面 完善社区活动页面 ...
- 深入解析volatile关键字
前言 很高兴遇见你~ 欢迎阅读我的文章. volatile关键字在Java多线程编程编程中起的作用是很大的,合理使用可以减少很多的线程安全问题.但其实可以发现使用这个关键字的开发者其实很少,包括我自己 ...
- python 通过pip freeze、dowload打离线包及自动安装【适用于保密的离线环境】
python的pip是其包管理工具,相当方便好用.本文只介绍pip 如何通过其freeze命令打离线包,及其离线包的安装脚本.这个知识点,特别适用于不适合连通互联网,设备需要物理隔绝,保密要求严格的客 ...
- shell,计算指定行的和,计算指定列的和
有一个文本文件,里面某行某列为数字,那么如何用shell计算指定行(列)的和,方法如下 计算指定行的和: awk 'NR==3{for(i=1;i<=NF;i++)sum=sum+$i;}END ...
- 密码管理平台ratticdb的部署,在centos7上的部署
一,前言 一直想用ratticdb这个有web界面的密码管理工具,百度了一下居然没有找到中文的部署文档,访问官网也是notfound.找到了官方的部署指南:https://github.com/til ...