本文将根据面试中常被问到的 Java线程池 展开抽丝剥茧的解析,这个问题可以说是百分之百会在Java程序员面试中被问到,因为在工作中这个需求实在是太普遍了。Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。
本文成文的思路将根据面试中问答的流程展开,读者完全可以将本文展开的知识点作为回答此问题的常规套路,如果你掌握本文所列出的知识点,那么就因这一个问题就可以让面试官对你刮目相看。
在被问到,你是否了解线程池时,这个毫无疑问,肯定都了解,那么从哪开始说呢?必须是使用线程的好处,也就是能解决什么问题。
先说出为什么需要使用线程池,也就是背景:
「因为创建一个线程,却需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,所以线程是一个重量级的对象,应该避免频繁创建和销毁,所以要使用线程池」
然后再说出线程池如果解决上面的问题,这里列举额有三个优点:
回答完使用线程池的必要性,接着就是重头戏,线程池的实现原理。
开门见山,先介绍线程池的实现类 ThreadPoolExecutor 是如何使用的,包含哪些参数,含义是什么?
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
要创建一个线程池,构造函数是比较复杂的,一共包含7个参数,具体每个参数的含义如下:
prestartAllCoreThreads()
方法,线程池会提前创建并启动所有基本线程。Executors.newFixedThreadPool()
使用了这个队列。Executors.newCachedThreadPool
使用了这个队列。我们再结合一张图给看看Java线程池的设计实现原理,帮你将上面线程池创建所需的参数串起来:
也就是说当线程池ThreadPoolExecutor执行execute或者submit方法时线程池的处理情况是这样的:
可以看出线程池是一个 「生产者-消费者」 模型。使用线程的一方是生产者,线程池本身是消费者。因为使用方是向线程池中丢任务(Runnable/Callable),而线程池本身消费这些提交的任务。
ThreadPoolExecutor 采取上述的实现原理,尽可能避免了在执行execute,submit方法时获取全局锁(性能瓶颈),因为只要提交的任务数达到 corePoolSize 时,几乎后面所有的 execute、submit 提交任务都是再走步骤2,无需获取锁,设计的是相当牛逼的。
在回答了线程池的实现原理后,那么具体如何使用呢?你就可以回答这两种向线程池提交任务的方式,以及他们之间的区别和使用场景。
一共有两种方法提交任务:
区别在于,execute 方法用于提交不需要返回值的任务,一方面无法判断任务是否执行成功,也无法获取线程的执行结果。
public void execute(Runnable command)
可以看到 execute接收的是一个 Runnable实例,并且这个方法是没有返回结果的。
那么你肯定会问,很多场景下,我们是需要获取任务的执行结果的。这种情况就可以使用 submit() 方法,ThreadPoolExecutor 提供了下面三个 submit 方法,方法签名如下:
// 提交Runnable任务
Future submit(Runnable task);
// 提交Callable任务
Future submit(Callable task);
// 提交Runnable任务及结果引用
Future submit(Runnable task, T result);
submit 方法都会返回 Future 对象,通过 future 对象的 future.isDone 方法我们就可以判断任务是否执行完成,以及通过 get() 和 get(timeout, unit) 获取任务的返回值。这两个get方法都会阻塞当前调用线程直到任务完成;
这里再多说一点,我们在平时使用Future阻塞获取任务结果时,可以使用 FutureTask 这个工具类,他同时实现了Runnable和Future接口,也就是说即可以作用任务传递给线程池,也可以用来获取子线程的执行结果。
示例如下:
// 创建FutureTask
FutureTask futureTask = new FutureTask<>(()->"这是返回结果" );
// 创建线程池
ExecutorService es = Executors.newCachedThreadPool();
// 提交FutureTask
es.submit(futureTask);
// 获取计算结果
String result = futureTask.get();
线程池的关闭,我们可以使用线程池的 shutdown 和 shutdownNow 方法。它俩的原理都是遍历线程池中的线程然后逐个调用线程的interrupt方法来中断线程,但是这块仅仅是调用中断方法,并不意味着线程就会终止,前提是线程执行的任务可以响应中断,要不然可能永远无法终止,对 线程中断不熟悉的小伙伴可以看看 七哥这篇踩坑指南:一个线程中断引发Bug的“爆肝”排查经历
但上面这两个方法还是存在一定差异的,即shutdownNow方法调用后,首先将线程池的状态设为 STOP,然后调用线程池中所有线程的interrupt方法(包含正在运行的线程),并且返回队列中等待执行任务的列表。而shutdown方法只是将线程状态设为SHUTDOWN状态,然后调用线程池中空闲线程的interrupt方法。
当你回答了上面所有线程池的知识点后,一般情况下已经差不多了,不过大厂的面试官还可能会问,既然你知道了原理,那么平时使用的时候,这些参数都是如何配置的呢?
遇到这个问题你的思路得清晰,这个数字肯定不是拍脑袋决定的,不要急于给出数字,而是从场景展开:
要想合理的使用线程池,那么就要首先分析任务特性,是CPU密集型还是IO密集型。
如果获取机器的CPU个数,我们可以使用Runtime.getRuntime().availableProcessors()
;
如果需要处理的任务是有优先级的,则可以使用PriorityBlockingQueue这个阻塞队列作为工作队列,优先级高的先执行。
值得一提的是,使用线程池建议使用有界队列,因为能增加系统的稳定性,我们之前就因为使用了 Executors.newFixedThreadPool() 创建的线程池,其默认使用了无界的 LinkedBlockingQueue 导致在数据库异常时,任务积压,线上频繁FGC,最终内存爆了,整个服务不可用。后来改为有界的工作队列后,就会不断抛出任务抛弃异常,便于监控发现并且不会导致整个服务不可用,只是线程任务异常。
本文基于面试场景,对于Java线程池展开了解析,相信你如果能将本文的内容做到了然于心,以后面试碰到Java线程池这个问题就再也不会垂头丧气,而且胸有成竹。
如果本文内容你觉得有所收获,请帮忙点个「三连」(转发、在看、点赞)
点个 在看
喜欢是一种感觉
在看是一种支持
↘↘↘