线程池常见面试题目_线程池面试
一,前言
关于线程池的问题,一直都是面试的重点,我结合一下自己的面试问题和部分网上搜的问题来谈一下线程池。
二,线程池是什么?
线程池就是一个管理线程的池子,我们新建一个线程去处理任务的时候,用完就销毁线程,难道不觉得很浪费吗,因为你创建线程和销毁线程是要时间和资源开销的。于是我们可以把这种线程池存起来,这样子既能复用,又能加快响应。线程池的参数主要有:
核心线程数(corePoolSize)
线程池中保持活动的最小线程数,即使空闲也不会被回收(除非设置allowCoreThreadTimeOut为true)。
最大线程数(maximumPoolSize)
线程池允许创建的最大线程数。当任务队列满且核心线程繁忙时,会创建新线程直至达到此值。
空闲线程存活时间(keepAliveTime)
非核心线程空闲时的存活时间,超时后会被回收(若核心线程允许超时,此规则也适用)。
时间单位(unit)
keepAliveTime的时间单位(如秒、毫秒)。
任务队列(workQueue)
存放待处理任务的阻塞队列,常见类型:
有界队列(如ArrayBlockingQueue):避免资源耗尽,但可能触发拒绝策略。
无界队列(如LinkedBlockingQueue):任务无限堆积,可能导致内存溢出。
同步移交队列(如SynchronousQueue):不存储任务,直接移交线程,需配合大maximumPoolSize。
线程工厂(threadFactory)
用于定制线程属性(如名称、优先级),便于问题排查。
拒绝策略(handler)
当线程池和队列均满时,处理新任务的策略,常见策略:
AbortPolicy:默认策略,抛出RejectedExecutionException。
CallerRunsPolicy:由提交任务的线程直接执行。
DiscardOldestPolicy:丢弃队列中最旧任务并重新提交。
DiscardPolicy:静默丢弃新任务。
如图:
其实理解很简单的,如图所示:
此外,我再举一个生活的例子帮助各位去理解:
场景设定:奶茶店配置:
常驻店员:2人(核心线程)
最大店员:5人(最大线程数)
排队区:3个座位(任务队列)
超时规则:临时工10分钟没活干就下班
爆满策略:挂\"停止接单\"牌子(拒绝策略)
状态模拟:
初始状态
早上开店时,只有2个常驻店员在柜台待命(corePoolSize=2),排队区空着。
顾客A、B进店
常驻店员1和2直接制作奶茶(核心线程立即处理任务)
顾客C,D,E进店
座位1、2、3被占满(任务进入队列),顾客坐着等待
顾客F进店
店长紧急呼叫临时工3号(创建新线程直到maxPoolSize=5)开始制作
顾客G进店
店长挂出\"停止接单\"牌子(触发拒绝策略),顾客G离开
高峰期过后
临时工3、4、5如果10分钟没新订单(keepAliveTime=10分钟),自动下班(ps:外包就是这样)
常驻店员1、2继续保持待命状态
上面就是常见面试题目之:Java的线程池说一下,各个参数的作用,如何进行的?
三,线程池异常之后,销毁还是复用?
直接说结论:使用execute()
时,未捕获异常导致线程终止,线程池创建新线程替代;使用submit()
时,异常被封装在Future
中,线程继续复用。面试官可能会追问:execute()和subbmit()的区别是什么?
回答:
execute()
:提交Runnable
任务,无返回值。
submit()
:可提交Callable
或Runnable
,返回Future
对象,可捕获异常。
四,任务队列有什么?
主要有下面五种:
1,ArrayBlockingQueue
原理:
基于数组的有界阻塞队列,必须指定容量大小(不可扩容),队列满时插入操作会阻塞,队列空时获取操作会阻塞。
特点:
固定容量,内存连续,性能稳定。
适合需要控制资源使用的场景(如线程池的任务队列)。
示例代码:
BlockingQueue queue = new ArrayBlockingQueue(10);
2,LinkedBlockingQueue
原理:
基于链表的可选有界阻塞队列(默认容量为 Integer.MAX_VALUE,近似无界)。
特点:
默认无界,但可以指定容量。
适合任务量未知但可能很大的场景(如 Executors.newFixedThreadPool 的默认队列)。
示例代码:
BlockingQueue queue = new LinkedBlockingQueue(100);
3,DelayQueue
原理:
无界阻塞队列,元素必须实现 Delayed 接口,只有延迟时间到期后才能被取出。
内部通过 PriorityQueue 按到期时间排序。
特点:
元素按延迟时间排序,延迟未到则 poll() 返回 null,take() 会阻塞。
实例代码:
class DelayTask implements Delayed { long executeTime; public long getDelay(TimeUnit unit) { return unit.convert(executeTime - System.currentTimeMillis(), MILLISECONDS); } public int compareTo(Delayed o) { return Long.compare(this.executeTime, ((DelayTask) o).executeTime); }}DelayQueue queue = new DelayQueue();
4,PriorityBlockingQueue
原理:
无界阻塞队列,支持优先级排序(元素需实现 Comparable 或传入 Comparator)。
适合需要按优先级处理任务的场景(如VIP,急诊排队系统)。
特点:
队列无界,但资源耗尽可能导致 OOM。迭代顺序不保证有序(仅保证出队顺序)。
示例代码:
BlockingQueue queue = new PriorityBlockingQueue(10, Comparator.reverseOrder());
5,SynchronousQueue
原理:
不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作,反之亦然。可理解为“一对一”直接传递数据的通道。
特点:
吞吐量高,适合传递性场景.
表格:
比较高频的面试问题:
为什么 ArrayBlockingQueue 和 LinkedBlockingQueue 的性能差异大?
Array 使用单锁,Linked 使用双锁,分离生产者和消费者的竞争。
DelayQueue 如何实现延迟获取元素?
内部通过 PriorityQueue 排序,take() 方法循环检查队首元素是否到期。
PriorityBlockingQueue 的排序是线程安全的吗?
是,通过锁保证插入和取出操作的原子性。
SynchronousQueue 和普通队列的区别?
不存储元素,直接传递,生产者和消费者必须成对出现。
如何避免 LinkedBlockingQueue 的 OOM 问题?
指定合理的队列容量,避免默认无界设置。
五,常用的线程池
newFixedThreadPool
特点:
固定大小线程池
核心线程数 = 最大线程数(固定线程数)
使用无界队列 LinkedBlockingQueue(默认容量Integer.MAX_VALUE)
实现:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());}
适用场景:
需要严格控制并发数的场景
长期稳定的轻量级任务处理
潜在风险:
无界队列可能导致OOM(任务堆积过多时),因为当线程数大于核心线程数的时候,就会加到阻塞队列中去,有因为使用了LinkedBlockingQueue,所以可能造成堆积。
newCachedThreadPool(可缓存线程的线程池)
特点
这个线程池的特点是没有核心线程,或者核心线程数为0,最大线程数是Integer.MAX_VALUE,这意味着线程池可以无限创建新线程。此外,它的空闲线程存活时间通常是60秒,超过这个时间没有任务的线程会被回收。
实现
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue());}
适用场景:
适合短期异步任务,高并发任务。
潜在风险
最大线程数无上限,可能创建大量线程导致资源耗尽。极端情况下可能导致创建百万线程,直接oom。并且假设提交的是比较长时间的任务,会导致线程无法回收。
newSingleThreadExecutor(单线程的线程池)
特点
核心线程数 = 最大线程数 = 1,使用无界队列 LinkedBlockingQueue
实现:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService( new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));}
适用场景:
需要保证任务顺序执行的场景
潜在风险:
OOM风险
newScheduledThreadPool(定时及周期执行的线程池)
特点:
最大线程数为Integer.MAX_VALUE,阻塞队列是DelayedWorkQueue,支持周期形的任务。
实现:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize);}// ScheduledThreadPoolExecutor继承关系public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService {}
场景:
时间调度,周期任务等。
潜在风险:
当任务执行速度 < 任务提交速度时,队列无限增长。会导致队列中堆积大量未执行任务,最终内存溢出。周期任务抛出异常后,后续执行自动终止且无日志
WorkStealingPool(工作窃取线程池)
特点
基于ForkJoinPool实现,并行处理任务。
实现:
public static ExecutorService newWorkStealingPool() { return new ForkJoinPool( Runtime.getRuntime().availableProcessors(), ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);}
场景:
大规模并行计算,CPU密集型任务
潜在风险
任务依赖死锁,任务划分不均。线程池设计初衷是处理CPU密集型任务,若混入I/O操作,所有工作线程被阻塞,无法窃取新任务。子任务抛出异常时,主任务可能无法感知
如何设置线程池的大小?
经典公式:
线程数 = CPU核数 * CPU利用率 * (1 + W/C)
现实中:
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
六,如果不允许线程池丢弃任务,应该选择哪个拒绝策略?
AbortPolicy(默认策略)
行为:直接抛出 RejectedExecutionException 异常,任务不会被处理。
问题:虽然任务未进入队列,但未实际执行,需外部捕获异常并处理(如重试),否则等同于丢弃。
CallerRunsPolicy(调用者执行)
行为:将任务回退给提交任务的线程(即调用 execute() 的线程)直接执行。
优点:任务不会被丢弃,且能自然限制任务提交速度(提交线程忙于执行任务时,会阻塞后续提交)。
DiscardOldestPolicy(丢弃最旧任务)
行为:丢弃队列中最旧的未处理任务,然后重新尝试提交当前任务。
问题:仍会丢弃任务(队列中的旧任务),违背“不丢弃”要求。
DiscardPolicy(静默丢弃)
行为:直接丢弃新任务,无任何通知。
问题:明显丢弃任务,不符合需求。
答:选择 CallerRunsPolicy,原因如下:
- 绝对不丢弃任务:无论队列和线程池状态如何,任务最终由调用线程执行,确保任务被处理。
- 自适应流量控制:调用线程直接执行任务时,会降低新任务提交速度(提交线程被占用),防止系统过载。