先赞后看,养成习惯 欢迎微信关注:Java编程之道,每天进步一点点,沉淀技术分享知识。
记一次真实蚂蚁金服
面试经历,这是鄙人在暑期找实习阶段遇到的社会主义爆锤!!!那年我还只是个懵懂的少年…
今天分享给需要秋(春)招面试的你们,看你们能抗住几个问题。你要都抗住了…
万字长文!!!一定要耐住看!看完血赚!
面试官:你了解多线程吗?线程池呢?
答:多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。同时也可以快速响应前端,将耗时任务交给线程去执行,提高前端用户的交互体验。
线程池是存放有一组线程的一个容器。线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。合理的使用线程池可以降低资源消耗,提高响应速度,提高线程的可管理性。
内心os:你看老弟我多稳!
面试官: 平时有用过线程池吗?
答:用过,曾设计线程池用于处理查询数据生产Excle文件并发送文件中心
的任务。提高了系统的吞吐能力和响应速度。
`内心os:问题不大,都是唠家常!``
面试官:JDK提供了哪些默认的线程池实现吗,大概有什么区别呢?记得多少说多少
答:newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
// 无限大小线程池 jvm自动回收
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int temp = i;
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName() + ",i:" + temp);
}
});
}
总结: 线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
final int temp = i;
newFixedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + ",i:" + temp);
}
});
}
总结:因为线程池大小为5,每个任务输出index其余任务会在队列种等待
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 10; i++) {
final int temp = i;
newScheduledThreadPool.schedule(new Runnable() {
public void run() {
System.out.println("i:" + temp);
}
}, 3, TimeUnit.SECONDS);
}
总结:表示延迟3秒执行。
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
newSingleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("index:" + index);
try {
Thread.sleep(200);
} catch (Exception e) {
// TODO: handle exception
}
}
});
}
注意: 结果依次输出,相当于顺序执行各个任务。
其实我当时回答的并没有现在这么全,主要还是记不起来名字了。
内心os:完了!没说全!会不会拜拜了?
面试官:阿里巴巴的java开发手册你看过吗?感觉写的怎么样?平时自己有按这个规范开发吗?
答:看过呀!阿里出品必属精品
!自己是按照规范来开发,现在基本上成了自己的代码习惯了,在IDEA里面也安装了代码规范校验插件。
内心os:峰回路转,重回正轨,先舔一下。
面试官:哦?看过啊!你知道为什么阿里不让使用默认的线程池实现方式吗?会出现OOME?
答:阿里规范里面强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
问题:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
出现OOME我觉得主要是因为以上创建线程池的方式没有限制队列大小,如果某一时间大量异步任务涌入必会使用大量线程来执行,极可能导致OOME,JVM中线程栈也有一个默认大小,没有限制的创建线程必定会占用大量资源同时GC可能并没有即时触发。
内心os:应该...说的靠谱吧!
面试官:那来介绍一下自定义线程池的几个常用参数呗?
答:
corePoolSize: 线程池的核心池大小,换句更精炼的话:corePoolSize表示允许线程池中允许同时运行的最大线程数。
maximumPoolSize: 线程池允许的最大线程数,他表示最大能创建多少个线程maximumPoolSize肯定是大于等于corePoolSize。
keepAliveTime: 表示线程没有任务时最多保持多久然后停止。默认情况下,只有线程池中线程数大于corePoolSize 时,keepAliveTime才会起作用。换句话说,当线程池中的线程数大于corePoolSize,并且一个线程空闲时间达到了keepAliveTime,那么才会shutdown。
unit: 参数keepAliveTime的时间单位。
workQueue 新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
ArrayBlockingQueue基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。
LinkedBlockingQuene基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
SynchronousQuene一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
PriorityBlockingQueue具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
threadFactory 创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
handler 当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制执行拒绝策略
内心os:都答上了,牛逼。问题不大。
面试官:简单说一下线程池的执行流程吧!
答:用户提交任务后会执行一下流程
内心os:还好看过源码写过博客,捞的一。
面试官:你这个几个参数的值是怎么得来的呀?算出来的?怎么算出来的?
答:我所知道的是IO密集型核心线程数是CPU数*2,计算密集型核心线程数是CPU数+1。最大线程数是(最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数。队列大小一般为(核心线程数/每个任务耗时时间)x 系统允许容忍的最大响应时间。
关于线程池最有大小还有一个公式:线程数=CPU数xCPU利用率x(1+等待时间/计算时间)。
内心os:求放过!
面试官:线程池里面的任务是IO密集型的还是计算密集型的呢?
答:我认为计算密集型就是计算、逻辑判断量非常大而且集中的类型,因为主要占用cpu资源所以又叫cpu密集型。IO密集型就是磁盘的读取数据和输出数据非常大的时候就是属于IO密集型。
内心os:应该靠谱!应该快结束了吧
面试官:那线程池创建的时候内部有多少可用的线程?啥时候才真正有活跃的线程呢?
答:0个。当调用prestartAllCoreThreads方法时,或者线程任务提交的时候才会有真正的线程。我大概跟你聊一下内部的细节吧。巴拉巴拉巴拉…
/**
* Set containing all worker threads in pool. Accessed only when
* holding mainLock.
*/
private final HashSet<Worker> workers = new HashSet<Worker>();
从注释可以看到这个HashSet是存放所有的工作线程的容器
,也就是线程池最核心的容器。我们可以就问题看看这个workers是在哪里进行Put操作的。我们看到ThreadPoolExecutor的构造函数中并没有对workers进行添加操作。只是对于变量进行了一个赋值操作,也就是说在ThreadPoolExecutor被new出来后workers容器里面是空的!也就是说初始线程为0
。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
那什么时候才创建了线程放在线程池中?我们知道提交任务无非两种方式execute和submit,那么我们从这里入手看看到底是怎么回事。
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
可以看到submit提交的任务最终都是走到了execute方法中。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
还是注释大法好,简单翻译一下
第一个启动新线程
打个断点看看?
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
Thread1 thread1 = new Thread1();
thread1.setName("线程一");
Thread2 thread2 = new Thread2();
thread2.setName("线程二");
executorService.submit(thread1);
executorService.submit(thread2);
executorService.shutdown();
}
我们分别在submit和newFixedThreadPool中打入断点
可以看到在线程池的构造方法执行结束后真正存放线程的set为0。
当代码执行到submit后进去到execute方法,才往容器中存放了一个工作线程。
内心os:还好看过源码,顶得住!
面试官:最后再问一个问题,一个线程池中的线程异常了,那么线程池会怎么处理这个线程?
答:首先线程池针对不同的提交方式会抛出堆栈异常如,execute方法会抛出异常submit不会。其次出现异常不会影响其他线程任务的执行,最后该异常线程会被清理,线程池会重新添加一个新线程作为补充!我简单的说一下源码,阿巴阿巴阿巴…
写个代码测试一下先
第一个结论得到证实:execute方法会抛出异常submit不会
。我们之道submit方法执行时,返回结果封装在future中,针对这种情况我们可future.get()方法进行异常捕获栈异常。我们来看看源码到底是为什么!
该方法是调取works中的任务来执行的地方。在submit调用执行的时候我们的代码执行到了下图位置
线程中的打印任务已经执行,讲道理出现异常了应该被catch了。
可以看到task被封装成了FutureTask
嗯?异常被单独处理了? setException(ex)咋处理的干啥了都。
再往下执行就基本上没东西了。卧槽?!那我们get一下试试?看看这个异常被处理的细节。
当我们调用get方法的时候state的值为3大于COMPLETING对应的2于是进入到report(s)方法。
在report方法中抛出了一个新的异常。由此可见我们在使用submit方法需要处理异常的时候需要对get方法进行异常的捕获!
出现异常后立即执行了异常的捕获,同时继续往下执行到 processWorkerExit(w, completedAbruptly)方法。
注释上可见该方法会移除异常线程,并创建一个新的线程去替换他。
面试官:好!基础不错!我们接下来聊聊微服务相关的,你了解分布式事务吗…
那年夏天我没死在多线程,我死在了微服务!那时候我真没学啊!我是真不会啊!能不能给我Offer先,后面在学嘛。厚着脸皮跟面试官聊了分布式存储,他说他不懂…卒!
关于线程池的面试点,常见的也就是这些了,你要都掌握了,然后老老实实的去看一下源码。这个时候能难住你的就只有货真价实的线程池/JVM调优了。
祝你好运!奥给力?力奥给?奥地利?奥力给!
欢迎各位好友前去关注!