0%

线程池

介绍一下线程池的工作原理

线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗

线程池分为核心线程池,线程池的最大容量,还有等待任务的队列,提交一个任务,如果核心线程没有满,就创建一个线程,如果满了,就是会加入等待队列,如果等待队列满了,就会增加线程,如果达到最大线程数量,就会按照一些丢弃的策略进行处理。

线程池的参数有哪些?

线程池的构造函数有7个参数:

  • corePoolSize:线程池核心线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。
  • maximumPoolSize:线程池中最多可容纳的线程数量。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程且当前线程池的线程数量小于maximumPoolSize,就会创建新的线程来执行任务,否则就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略。
  • keepAliveTime:当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。
  • unit:就是keepAliveTime时间的单位。
  • workQueue:工作队列。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。
  • threadFactory:线程工厂。可以用来给线程取名字等等
  • handler:拒绝策略。

线程池工作队列满了有哪些拒绝策略?

当线程池的任务队列满了之后,线程池会执行指定的拒绝策略来应对,常用的四种拒绝策略包括:
CallerRunsPolicy、AbortPolicy、DiscardPolicy、DiscardOldestPolicy,此外,还可以通过实现RejectedExecutionHandler接口来自定义拒绝策略。

四种预置的拒绝策略:

  • CallerRunsPolicy,使用线程池的调用者所在的线程去执行被拒绝的任务,除非线程池被停止或者线程池的任务队列已有空缺。
  • AbortPolicy,直接抛出一个任务被线程池拒绝的异常。
  • DiscardPolicy,不做任何处理,静默拒绝提交的任务。
  • DiscardOldestPolicy,抛弃最老的任务,然后执行该任务。
  • 自定义拒绝策略,通过实现接口可以自定义任务拒绝策略。

有线程池参数设置的经验吗?

核心线程数(corePoolSize)设置的经验:

  • CPU密集型:corePoolSize = CPU核数 + 1(避免过多线程竞争CPU)
  • IO密集型:corePoolSize = CPU核数 x 2(或更高,具体看IO等待时间)

场景一:电商场景,特点瞬时高并发、任务处理时间短,线程池的配置可设置如下:

1
2
3
4
5
6
7
new ThreadPoolExecutor(
16, // corePoolSize = 16(假设8核CPU × 2)
32, // maximumPoolSize = 32(突发流量扩容)
10, TimeUnit.SECONDS, // 非核心线程空闲10秒回收
new SynchronousQueue<>(), // 不缓存任务,直接扩容线程
new AbortPolicy() // 直接拒绝,避免系统过载
);

说明:

  • 使用 SynchronousQueue 确保任务直达线程,避免队列延迟。
  • 拒绝策略快速失败,前端返回“活动火爆”提示,结合降级策略(如缓存预热)。

场景二:后台数据处理服务,特点稳定流量、任务处理时间长(秒级)、允许一定延迟,线程池的配置可设置如下:

1
2
3
4
5
6
7
new ThreadPoolExecutor(
8, // corePoolSize = 8(8核CPU)
8, // maximumPoolSize = 8(禁止扩容,避免资源耗尽)
0, TimeUnit.SECONDS, // 不回收线程
new ArrayBlockingQueue<>(1000), // 有界队列,容量1000
new CallerRunsPolicy() // 队列满后由调用线程执行
);

说明:

  • 固定线程数避免资源波动,队列缓冲任务,拒绝策略兜底。
  • 配合监控告警(如队列使用率>80%触发扩容)。

场景三:微服务HTTP请求处理,特点IO密集型、依赖下游服务响应时间,线程池的配置可设置如下:

1
2
3
4
5
6
7
new ThreadPoolExecutor(
16, // corePoolSize = 16(8核 × 2)
64, // maximumPoolSize = 64(应对慢下游)
60, TimeUnit.SECONDS, // 非核心线程空闲60秒回收
new LinkedBlockingQueue<>(200), // 有界队列容量200
new CustomRetryPolicy() // 自定义拒绝策略(重试或降级)
);

说明:

  • 根据下游RT(响应时间)调整线程数,队列防止瞬时峰值。
  • 自定义拒绝策略将任务暂存Redis,异步重试。

核心线程数设置为0可不可以?

可以,当核心线程数为0的时候,会创建一个非核心线程进行执行。
从下面的源码也可以看到,当核心线程数为 0 时,来了一个任务之后,会先将任务添加到任务队列,同时也会判断当前工作的线程数是否为 0,如果为 0,则会创建线程来执行线程池的任务。

线程池种类有哪些?

  • ScheduledThreadPool:可以设置定期的执行任务,它支持定时或周期性执行任务,比如每隔 10 秒钟执行一次任务,我通过这个实现类设置定期执行任务的策略。
  • FixedThreadPool:它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了。
  • CachedThreadPool:可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 2^31-1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。
  • SingleThreadExecutor:它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的
  • SingleThreadScheduledExecutor:它实际和 ScheduledThreadPool 线程池非常相似,它只是ScheduledThreadPool 的一个特例,内部只有一个线程。

线程池一般是怎么用的?

Java 中的 Executors 类定义了一些快捷的工具方法,来帮助我们快速创建线程池。《阿里巴巴 Java 开发手册》中提到,禁止使用这些方法来创建线程池,而应该手动 new ThreadPoolExecutor 来创建线程池。这一条规则的背后,是大量血淋淋的生产事故,最典型的就是 newFixedThreadPool 和newCachedThreadPool,可能因为资源耗尽导致 OOM 问题。

所以,不建议使用 Executors 提供的两种快捷的线程池,原因如下:

  • 我们需要根据自己的场景、并发情况来评估线程池的几个核心参数,包括核心线程数、最大线程数、线程回收策略、工作队列的类型,以及拒绝策略,确保线程池的工作行为符合需求,一般都需要设置有界的工作队列和可控的线程数。
  • 任何时候,都应该为自定义线程池指定有意义的名称,以方便排查问题。当出现线程数量暴增、线程死锁、线程占用大量 CPU、线程执行出现异常等问题时,我们往往会抓取线程栈。此时,有意义的线程名称,就可以方便我们定位问题。

除了建议手动声明线程池以外,我还建议用一些监控手段来观察线程池的状态。线程池这个组件往往会表现得任劳任怨、默默无闻,除非是出现了拒绝策略,否则压力再大都不会抛出一个异常。如果我们能提前观察到线程池队列的积压,或者线程数量的快速膨胀,往往可以提早发现并解决问题。

线程池中shutdown (),shutdownNow()这两个方法有什么作用?

  • shutdown使用了以后会置状态为SHUTDOWN,正在执行的任务会继续执行下去,没有被执行的则中断。此时,则不能再往线程池中添加任何任务,否则将会抛出 RejectedExecutionException 异常
  • shutdownNow 为STOP,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。 它试图终止线程的方法是通过调用 Thread.interrupt() 方法来实现的,但是这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用,interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成

提交给线程池中的任务可以被撤回吗?

可以,当向线程池提交任务时,会得到一个 Future 对象。这个 Future 对象提供了几种方法来管理任务的执行,包括取消任务。

取消任务的主要方法是 Future 接口中的 cancel(boolean mayInterruptIfRunning) 方法。这个方法尝试取消执行的任务。参数 mayInterruptIfRunning 指示是否允许中断正在执行的任务。如果设置为true ,则表示如果任务已经开始执行,那么允许中断任务;如果设置为 false ,任务已经开始执行则不会被中断。