> 技术文档 > 线程池常见面试题目_线程池面试

线程池常见面试题目_线程池面试


一,前言

关于线程池的问题,一直都是面试的重点,我结合一下自己的面试问题和部分网上搜的问题来谈一下线程池。

二,线程池是什么?

线程池就是一个管理线程的池子,我们新建一个线程去处理任务的时候,用完就销毁线程,难道不觉得很浪费吗,因为你创建线程和销毁线程是要时间和资源开销的。于是我们可以把这种线程池存起来,这样子既能复用,又能加快响应。线程池的参数主要有:

核心线程数(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():可提交CallableRunnable,返回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 链表 可选有界 双锁(分离锁) 高吞吐任务队列 DelayQueue 堆 无界 锁 + 优先级排序 延迟任务调度(定时任务) PriorityBlockingQueue 堆 无界 锁 + 自动扩容 按优先级处理任务 SynchronousQueue 无 不存储 CAS 或双栈/队列 直接传递任务(线程池匹配)
比较高频的面试问题:

为什么 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,原因如下:

  1. 绝对不丢弃任务:无论队列和线程池状态如何,任务最终由调用线程执行,确保任务被处理。
  2. 自适应流量控制:调用线程直接执行任务时,会降低新任务提交速度(提交线程被占用),防止系统过载。