【JUC面试篇】Java并发编程高频八股——线程池
目录
1. 为什么要用线程池(线程池的好处)?
2. 如何创建线程池?
3. 为什么不推荐使用内置线程池?
4. 线程池常见参数有哪些?如何解释?
5. 线程池的拒绝策略有哪些?
6. 线程池处理任务的流程?线程池的工作原理?
7. 线程池中线程异常后,销毁还是复用( execute() 与 submit()的区别)?
8. 如何给线程池命名?
9. 如何动态修改线程池的参数?
10. 如何设计一个能够根据任务的优先级来执行的线程池?
11. 有线程池参数设置的经验吗,参数设置技巧?
12. 核心线程数设置为0可不可以?
13. 线程池种类有哪些?
14. 线程池中shutdown (),shutdownNow()这两个方法有什么作用?
15. 提交给线程池中的任务可以被撤回吗?
1. 为什么要用线程池(线程池的好处)?
线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
2. 如何创建线程池?
【JUC】并发编程重点知识——线程池
3. 为什么不推荐使用内置线程池?
《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors
去创建,而是通过 ThreadPoolExecutor
构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors
返回线程池对象的弊端如下:
FixedThreadPool
和SingleThreadExecutor
:使用的是阻塞队列LinkedBlockingQueue
,任务队列最大长度为Integer.MAX_VALUE
,可以看作是无界的,可能堆积大量的请求,从而导致 OOM。CachedThreadPool
:使用的是同步队列SynchronousQueue
, 允许创建的线程数量为Integer.MAX_VALUE
,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。ScheduledThreadPool
和SingleThreadScheduledExecutor
:使用的无界的延迟阻塞队列DelayedWorkQueue
,任务队列最大长度为Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。
4. 线程池常见参数有哪些?如何解释?
【JUC】并发编程重点知识——线程池
5. 线程池的拒绝策略有哪些?
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolExecutor
定义一些策略:
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理。ThreadPoolExecutor.CallerRunsPolicy
:调用执行者自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。ThreadPoolExecutor.DiscardPolicy
:不处理新任务,直接丢弃掉。ThreadPoolExecutor.DiscardOldestPolicy
:此策略将丢弃最早的未处理的任务请求。
【JUC】并发编程重点知识——线程池
6. 线程池处理任务的流程?线程池的工作原理?
- 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
- 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
- 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
- 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用
RejectedExecutionHandler.rejectedExecution()
方法。
【JUC】并发编程重点知识——线程池
7. 线程池中线程异常后,销毁还是复用( execute() 与 submit()的区别
)?
直接说结论,需要分两种情况:
- 使用
execute()
提交任务:当任务通过execute()
提交到线程池并在执行过程中抛出异常时,如果这个异常没有在任务内被捕获,那么该异常会导致当前线程终止,并且异常会被打印到控制台或日志文件中。线程池会检测到这种线程终止,并创建一个新线程来替换它,从而保持配置的线程数不变。 - 使用
submit()
提交任务:对于通过submit()
提交的任务,如果在任务执行中发生异常,这个异常不会直接打印出来。相反,异常会被封装在由submit()
返回的Future
对象中。当调用Future.get()
方法时,可以捕获到一个ExecutionException
。在这种情况下,线程不会因为异常而终止,它会继续存在于线程池中,准备执行后续的任务。
简单来说:使用execute()
时,未捕获异常导致线程终止,线程池创建新线程替代;使用submit()
时,异常被封装在Future
中,线程继续复用。
这种设计允许submit()
提供更灵活的错误处理机制,因为它允许调用者决定如何处理异常,而execute()
则适用于那些不需要关注执行结果的场景。
8. 如何给线程池命名?
初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题。
默认情况下创建的线程名字类似 pool-1-thread-n
这样的,没有业务含义,不利于我们定位问题。
给线程池里的线程命名通常有下面两种方式:
1、利用 guava 的 ThreadFactoryBuilder
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + "-%d").setDaemon(true).build();
ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
2、自己实现 ThreadFactory
。
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;/*** 线程工厂,它设置线程名称,有利于我们定位问题。*/
public final class NamingThreadFactory implements ThreadFactory {private final AtomicInteger threadNum = new AtomicInteger();private final String name;/*** 创建一个带名字的线程池生产工厂*/public NamingThreadFactory(String name) {this.name = name;}@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r);t.setName(name + " [#" + threadNum.incrementAndGet() + "]");return t;}
}
9. 如何动态修改线程池的参数?
三个核心参数:
corePoolSize
: 核心线程数线程数定义了最小可以同时运行的线程数量。maximumPoolSize
: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
如何支持参数动态配置? 且看 ThreadPoolExecutor
提供的下面这些方法。
格外需要注意的是corePoolSize
, 程序运行期间的时候,我们调用 setCorePoolSize()
这个方法的话,线程池会首先判断当前工作线程数是否大于corePoolSize
,如果大于的话就会回收工作线程。
另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 ResizableCapacityLinkedBlockIngQueue
的队列(主要就是把LinkedBlockingQueue
的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。
10. 如何设计一个能够根据任务的优先级来执行的线程池?
不同的线程池会选用不同的阻塞队列作为任务队列,比如FixedThreadPool
使用的是LinkedBlockingQueue
(有界队列),默认构造器初始的队列长度为 Integer.MAX_VALUE
,由于队列永远不会被放满,因此FixedThreadPool
最多只能创建核心线程数的线程。
假如我们需要实现一个优先级任务线程池的话,那可以考虑使用 PriorityBlockingQueue
(优先级阻塞队列)作为任务队列(ThreadPoolExecutor
的构造函数有一个 workQueue
参数可以传入任务队列)。
PriorityBlockingQueue
是一个支持优先级的无界阻塞队列,可以看作是线程安全的 PriorityQueue
,两者底层都是使用小顶堆形式的二叉堆,即值最小的元素优先出队。不过,PriorityQueue
不支持阻塞操作。
要想让 PriorityBlockingQueue
实现对任务的排序,传入其中的任务必须是具备排序能力的,方式有两种:
- 提交到线程池的任务实现
Comparable
接口,并重写compareTo
方法来指定任务之间的优先级比较规则。 - 创建
PriorityBlockingQueue
时传入一个Comparator
对象来指定任务之间的排序规则(推荐)。
不过,这存在一些风险和问题,比如:
PriorityBlockingQueue
是无界的,可能堆积大量的请求,从而导致 OOM。- 可能会导致饥饿问题,即低优先级的任务长时间得不到执行。
- 由于需要对队列中的元素进行排序操作以及保证线程安全(并发控制采用的是可重入锁
ReentrantLock
),因此会降低性能。
对于 OOM 这个问题的解决比较简单粗暴,就是继承PriorityBlockingQueue
并重写一下 offer
方法(入队)的逻辑,当插入的元素数量超过指定值就返回 false 。
饥饿问题这个可以通过优化设计来解决(比较麻烦),比如等待时间过长的任务会被移除并重新添加到队列中,但是优先级会被提升。
对于性能方面的影响,是没办法避免的,毕竟需要对任务进行排序操作。并且,对于大部分业务场景来说,这点性能影响是可以接受的。
11. 有线程池参数设置的经验吗,参数设置技巧?
核心线程数(corePoolSize)设置的经验:
- CPU密集型:corePoolSize = CPU核数 + 1(避免过多线程竞争CPU)
- IO密集型:corePoolSize = CPU核数 x 2(或更高,具体看IO等待时间)
场景一:电商场景,特点瞬时高并发、任务处理时间短,线程池的配置可设置如下:
new ThreadPoolExecutor(16, // corePoolSize = 16 (假设8核CPU × 2)32, // maximumPoolSize = 32 (突发流量扩容)10, TimeUnit.SECONDS, // 非核心线程空闲10秒回收new SynchronousQueue<>(), // 不缓存任务,直接扩容线程new AbortPolicy() // 直接拒绝,避免系统过载
);
说明:
- 使用SynchronousQueue确保任务直达线程,避免队列延迟。
- 拒绝策略快速失败,前端返回“活动火爆”提示,结合降级策略(如缓存预热)。
场景二:后台数据处理服务,特点稳定流量、任务处理时间长(秒级)、允许一定延迟,线程池的配置可设置如下:
new ThreadPoolExecutor(8, // corePoolSize = 8 (8核CPU)8, // maximumPoolSize = 8 (禁止扩容, 避免资源耗尽)0, TimeUnit.SECONDS, // 不回收线程new ArrayBlockingQueue<>(1000), // 有界队列, 容量1000new CallerRunsPolicy() // 队列满后由调用线程执行
);
说明:
- 固定线程数避免资源波动, 队列缓冲任务, 拒绝策略兜底。
- 配合监控告警(如队列使用率>80%触发扩容)。
场景三:微服务HTTP请求处理, 特点IO密集型、依赖下游服务响应时间, 线程池的配置可设置如下:
new ThreadPoolExecutor(16, // corePoolSize = 16 (8核 × 2)64, // maximumPoolSize = 64 (应对慢下游)60, TimeUnit.SECONDS, // 非核心线程空闲60秒回收new LinkedBlockingQueue<>(200), // 有界队列容量200new CustomRetryPolicy() // 自定义拒绝策略 (重试或降级)
);
说明:
- 根据下游RT (响应时间) 调整线程数,队列防止瞬时峰值。
- 自定义拒绝策略将任务暂存Redis, 异步重试。
12. 核心线程数设置为0可不可以?
可以,当核心线程数为0的时候,会创建一个非核心线程进行执行。
当核心线程数为0时,来了一个任务之后,会先将任务添加到任务队列,同时也会判断当前工作的线程数是否为 0,如果为 0,则会创建线程来执行线程池的任务。
参考CachedThreadPool
13. 线程池种类有哪些?
- ScheduledThreadPool:可以设置定期的执行任务,它支持定时或周期性执行任务,比如每隔 10 秒钟执行一次任务,我通过这个实现类设置定期执行任务的策略。
- FixedThreadPool:它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了。
- CachedThreadPool:可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 231−1231−1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为 0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。
- SingleThreadExecutor:它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的。
- SingleThreadScheduledExecutor:它实际和 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程。
14. 线程池中shutdown (),shutdownNow()这两个方法有什么作用?
从下面的源码【高亮】注释可以很清晰的看出两者的区别:
- shutdown使用了以后,会置状态为SHUTDOWN,正在执行的任务会继续执行下去,没有被执行的则中断。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常
- 而shutdownNow为STOP,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是这种方法的作用有限,如果线程中没有sleep、wait、Condition、定时锁等应用,interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。
shutdown源码:
public void shutdown() {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {checkShutdownAccess();// 高亮advanceRunState(SHUTDOWN);interruptIdleWorkers();onShutdown();} finally {mainLock.unlock();}tryTerminate();
}
shutdownNow源码:
public List<Runnable> shutdownNow() {List<Runnable> tasks;final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {checkShutdownAccess();// 高亮advanceRunState(STOP);interruptWorkers();// 高亮tasks = drainQueue();} finally {mainLock.unlock();}tryTerminate();// 高亮return tasks;
}
简单总结:
shutdown()
:关闭线程池,线程池的状态变为SHUTDOWN
。线程池不再接受新任务了,但是队列里的任务得执行完毕。shutdownNow()
:关闭线程池,线程池的状态变为STOP
。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
15. 提交给线程池中的任务可以被撤回吗?
可以,当向线程池提交任务时,会得到一个Future
对象。这个Future
对象提供了几种方法来管理任务的执行,包括取消任务。
取消任务的主要方法是Future
接口中的cancel(boolean mayInterruptIfRunning)
方法。这个方法尝试取消执行的任务。参数mayInterruptIfRunning
指示是否允许中断正在执行的任务。如果设置为true
,则表示如果任务已经开始执行,那么允许中断任务;如果设置为false
,任务已经开始执行则不会被中断。
public interface Future<V>{// 是否取消线程的执行boolean cancel(boolean mayInterruptIfRunning);// 线程是否被取消boolean isCancelled();//线程是否执行完毕boolean isDone();// 立即获得线程返回的结果V get() throws InterruptedException, ExecutionException;// 延时时间后再获得线程返回的结果V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;
}
取消线程池中任务的方式,代码如下,通过future
对象的cancel(boolean)
函数来定向取消特定的任务。
public static void main(String[] args) {ExecutorService service = Executors.newSingleThreadExecutor();Future future = service.submit(new TheradDemo());try {// 可能抛出异常future.get();} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}finally {//终止任务的执行future.cancel(true);}
}