大家好,我是程序员牛肉。
上周朋友在面试字节跳动的时候,遇到了这样一个问题:线程池中的核心线程在没有接到任务的时候是哪种状态?
直接给我朋友问懵了。当他把这个问题分享给我的时候,我也有点懵逼。因此我专门整理了一些关于线程池的面试问题,希望这些问题可以让大家对线程池有更加深入的理解
今天的问题包括:
[1.线程池中的核心线程在没有接到任务的时候是哪种状态?
2.ThreadPoolExecutor线程池有几种运行状态?
3.使用线程池的时候,submit和excute有什么区别?
4.线程池中的线程出现异常之后,这个线程会被销毁还是继续复用?
5.使用shutdown和shutdownNow来关闭线程池的区别是什么?
6.线程池是如何区分一个线程是核心线程还是非核心线程的?]
1.线程池中的核心线程在没有接到任务的时候是哪种状态?
核心线程如果设置的不会被回收且核心线程池已经被填满(未满的情况下任务提交是不会经过队列的),那么在空闲时会处于Waiting状态,等待任务到来时调用Support.unpark()方法转变为Runnable状态。
这块碍于篇幅问题就不展示源代码了,层级有点深。感兴趣的同学可以从ThreadPoolExecutor中的gettask方法开始追:
我标出的这一行的代码逻辑就是:
如果timed为true:使用poll方法从工作队列this.workQueue中获取一个任务。poll方法会等待指定的时间(this.keepAliveTime纳秒),如果在这段时间内没有任务可用,它会返回null。
如果timed为false:使用take方法从工作队列this.workQueue中获取一个任务。take方法会阻塞当前线程,直到从队列中获取到一个任务或者线程被中断。
[timed为true:
这意味着线程池允许核心线程超时(this.allowCoreThreadTimeOut为true),或者当前工作线程数(wc)大于核心线程数(this.corePoolSize)。在这种情况下,线程会使用poll方法来尝试从工作队列中获取任务。poll方法会等待一个指定的时间(this.keepAliveTime纳秒),如果在这段时间内没有任务可用,它会返回null,而不是无限期地等待。
timed为false:
这意味着线程池不允许核心线程超时(this.allowCoreThreadTimeOut为false),并且当前工作线程数不超过核心线程数(wc <= this.corePoolSize)。在这种情况下,线程会使用take方法来从工作队列中获取任务。take方法会阻塞当前线程,直到从队列中获取到一个任务或者线程被中断。]
如果你追到最后,你就会看到一个park方法,他就实现了令线程池的核心线程进入阻塞状态。如果你读过Reentrantlock的源码,相信你对这个方法会更加熟悉。因为Reentrantlock的公平锁的实现就使用了park()。
2.ThreadPoolExecutor线程池有几种运行状态?
没错,线程池也有状态。在源码中一共为ThreadPoolExecutor线程池定义了五种线程池状态。
线程池的状态通常有以下几种:
这些状态的变化通常是单向的,即从运行中到关闭,再到停止,然后是阻塞和终止。一旦线程池进入终止状态,它就不能被重新启动或回到之前的状态。
让我们用一张图来说明一下线程池的状态流转:
3.使用线程池的时候,submit和excute有什么区别?
1.excute方法:
ThreadPoolExecutor executor = new ThreadPoolExecutor();
executor.execute();
返回值:excute方法没有返回值。
使用场景:execute方法适用于提交不需要返回值结果的任务,它将任务提交给线程池执行之后不会返回关于任务的任何结果或状态。并且其只能提交Runnable接口的任务。
异常处理:如果任务在执行过程中抛出未捕获的异常,那么线程池就会把当前异常传递给当前线程的UnCaughExceptionHandler。
2.submit方法:
ThreadPoolExecutor executor = new ThreadPoolExecutor();
executor.submit();
返回值:submit会返回一个Future对象,通过这个Future对象可以检查任务的状态,取消任务以及获取任务的结果。
使用场景:submit适用于提交需要返回结果的任务。它可以提交Runnable以及Callable接口的任务。
异常处理:如果任务在执行的时候抛出异常,异常会被捕获并存储在返回的Future对象中。我们可以通过Future.get来获取异常。
4.线程池中的线程出现异常之后,这个线程会被销毁还是继续复用?
这个还是要分情况看,分为是使用submit还是使用excute来提交任务。先总体概括一下就是:
当使用execute()方法向线程池提交任务时,如果任务在执行过程中抛出未被捕获的异常,这将导致执行任务的线程终止,并且异常信息会被输出到控制台或日志文件。在这种情况下,线程池会监测到线程的终止,并自动创建一个新的线程来补充,以确保线程池中的线程数量维持在设定的配置数量。
而当任务通过submit()方法提交时,如果在执行过程中发生异常,异常不会被直接输出。异常会被封装在submit()返回的Future对象中。当调用这个Future对象的get()方法时,可以通过捕获ExecutionException来处理这个异常。在这种情形下,执行任务的线程不会因异常而终止,它将保持在线程池中,准备接受并执行后续的任务。
我们来看一下源码:
当使用execute的时候:
execute使用addWorker来提交任务,使用RunWorker来执行任务,因此我们转到RunWorker中:
一大串代码看不懂不要紧,能看懂这个try-finally就行了。所有的异常被抛出之后都会执行这个processWorkerExit方法
这个方法先remove一个worker后,又使用addWorker来重新添加了一个worker。
因此execute的逻辑是:当线程出现异常之后,选择废弃当前线程,重新重建一个。
当使用submit的时候:
我们会发现提交一个submit之后,会调用这个newTaskFor将其包装成为一个RunnableFuture之后提交给execute方法。因此我们去看一看这个RunnableFuture做了哪些处理:
在这段代码当中我们把异常捕捉了。也就是说在调用excute的时候,就不会被try-catch捕捉到异常。那么也就没有办法执行excute中的processWorkerExit方法。
因此当在这种情况下,线程不会因为异常而终止,它会继续存在于线程池中,准备执行后续的任务。
5.使用shutdown和shutdownNow来关闭线程池的区别是什么?
这个问题其实基于我们前面介绍的线程池的五种状态就很好回答。调用shutdown之后会进入shutdown状态,而调用shutdownNow之后会进入Stop状态。因此回答这个问题其实就是在回答shutdown状态和stop状态的区别。
也就是说使用shutdown关闭线程池后,线程池不会再接收新任务,当处理完阻塞队列中的任务后完全关闭线程池。
而当使用shutdownNow关闭线程池后,线程池尝试立即停止所有正在执行的任务,并返回一个列表,包含那些尚未执行的任务。这个方法会尝试中断所有正在执行的任务,但是是否能够立即停止取决于任务的实现。如果任务没有正确处理中断,它们可能不会立即停止。
6.线程池是如何区分一个线程是核心线程还是非核心线程的?
当非核心线程在达到最大存活时间且仍没有任务需要处理的时候,我们的线程池就要回收这些非核心线程。那么我们如何确保不会错误销毁核心线程呢?
事实上线程池根本就没区分核心线程和非核心线程。因此在销毁的时候实际上是随机销毁。
比如核心线程是3个,非核心线程是2个。那我就随机删除这个5个线程中的2个线程。存活下来线程就是核心线程。
今天关于线程池的刁钻问题就到这里了,不知道这些问题有没有把你搞晕。希望我的文章可以帮到你。
你对于线程池还有哪些独到的理解呢?欢迎在评论区留言。
关注我,带你了解更多计算机干货。
end