引言
如果大家在简历中写熟悉Java并发编程或者项目有牵扯到线程池相关内容,那么被拷打线程池是大概率的事。下面的内容是有关今年五月份某大厂面试中有关线程池的拷打,我从中提取一些比较通用的内容进行解答,让我们一起看看吧!
线程池的好处😯
经典起手,这就非常八股了,这里我想让大家都明白一个道理,生活和学习中,都要先明白做这件事的好处(坏处)是什么,再想如何做可能灵感就会多得多,好的下面让我们回归正题❤️。
在Java开发中,使用线程池(如通过ThreadPoolExecutor
类实现)有多个好处:
- 重用线程:通过线程池可以重用已创建的线程。如果不使用线程池,每次需要执行一个任务时都必须创建一个新的线程,在任务完成后销毁该线程。这种做法会导致大量的资源消耗和性能损耗。而线程池允许线程被重复利用,减少了线程创建和销毁的开销。
- 控制并发线程数量:线程池可以帮助你限制系统中并发执行的线程数量。通过设定最大线程数,你可以避免因为过多的线程同时运行而导致系统过载的问题。
- 管理队列:当所有线程都在忙碌时,新的任务会被放入等待队列中,直到有可用的线程来处理它们。这有助于平滑负载峰值。
- 提高响应速度:由于线程已经被创建并就绪,因此当有新任务到达时可以立即开始执行,不需要经历线程创建的延迟,从而提高了应用对请求的响应速度。
- 方便的管理功能 :
ThreadPoolExecutor
提供了多种管理和监控线程池的方法,比如获取当前活跃线程数、关闭线程池等。 - 调度能力 :结合
ScheduledThreadPoolExecutor
,还可以实现定时或周期性的任务执行。
ThreadPoolExecutor
是Java提供的一个灵活的线程池实现,它允许开发者根据具体需求配置核心线程数、最大线程数、空闲线程存活时间、工作队列类型等参数。这些特性使得它成为处理大量异步任务执行的理想选择。
怎么创建一个线程池🤗
在Java中创建一个线程池可以通过java.util.concurrent.Executors
工厂类或者直接实例化ThreadPoolExecutor
来实现。下面是两种不同的方法:
使用Executors工厂类
Executors
提供了一系列静态工厂方法来创建不同类型的线程池,这些方法包括但不限于:
-
固定大小的线程池:
javaExecutorService executorService = Executors.newFixedThreadPool(10);
这个方法会创建一个拥有10个线程的线程池,如果所有线程都在忙,额外的任务将会在队列中等待。
-
缓存型线程池:
javaExecutorService executorService = Executors.newCachedThreadPool();
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的应用程序来说,这类线程池是理想选择。
-
单线程化的线程池:
javaExecutorService executorService = Executors.newSingleThreadExecutor();
创建一个只有一个工作线程的线程池,以无顺序的方式一个接一个地执行任务。
-
调度线程池:
javaScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
创建一个支持定时和周期性任务执行的线程池,具有指定的核心线程数。
直接使用ThreadPoolExecutor
如果你想更细致地控制线程池的行为,可以直接使用ThreadPoolExecutor
的构造函数,它提供了最大的灵活性:
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // 核心线程数
20, // 最大线程数
1, TimeUnit.MINUTES, // 线程空闲时间
new LinkedBlockingQueue<Runnable>(100), // 工作队列
new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
这里,
- 核心线程数是指线程池中的常驻线程数。
- 最大线程数是指线程池中允许存在的最大线程数。
- 线程空闲时间是指超过核心线程数的那些线程,在空闲了指定的时间后会被终止。
- 工作队列用于保存等待执行的任务。
- 拒绝策略定义了当任务无法被提交到线程池时的行为。
创建好线程池之后,你可以通过调用execute()
或submit()
方法来向线程池提交任务。记得在线程池不再使用时调用shutdown()
方法来关闭线程池,确保程序可以正常退出。
线程池拒绝策略🤓
在Java中,当线程池无法接受新任务时(例如,当所有线程都在忙碌且工作队列已满),需要一种策略来处理这种情况。这种策略被称为线程池的拒绝策略,它由RejectedExecutionHandler
接口定义,并且Java提供了几种内置的实现:
-
AbortPolicy(默认策略) :
- 抛出一个
RejectedExecutionException
异常。这可以让你捕获异常并进行相应的处理。
javaThreadPoolExecutor.AbortPolicy()
- 抛出一个
-
CallerRunsPolicy:
- 不在线程池中的线程执行任务,而是直接在调用者线程中运行被拒绝的任务。这种方式提供了一种简单的反馈控制机制,减缓了新任务的提交速度。
javaThreadPoolExecutor.CallerRunsPolicy()
-
DiscardPolicy:
- 直接丢弃被拒绝的任务,不做任何处理也不会抛出异常。这种策略适用于当你不需要关心任务丢失的情况。
javaThreadPoolExecutor.DiscardPolicy()
-
DiscardOldestPolicy:
- 如果线程池没有关闭,则将工作队列中的第一个任务(即最旧的那个未处理的任务)丢弃,然后尝试重新提交被拒绝的任务。注意,这个策略不会保证任务的顺序执行,因为它可能导致较早提交的任务被后来的任务取代。
javaThreadPoolExecutor.DiscardOldestPolicy()
你可以通过以下方式为ThreadPoolExecutor
设置自定义的拒绝策略:
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
workQueue,
new ThreadPoolExecutor.DiscardPolicy());
其中,new ThreadPoolExecutor.DiscardPolicy()
可以替换为你选择的任何一种拒绝策略。这样做可以帮助你更好地管理线程池的行为,尤其是在高负载的情况下。
预热线程池🤠
熟悉线程池任务执行流程的都清楚,线程池刚创建时,里面并没有核心线程数的,是一个任务来,才创建一个核心线程,直到到核心线程数。
在Java中,ThreadPoolExecutor
允许你通过预先启动核心线程数来"预热"线程池。这意味着即使没有任务提交到线程池,核心线程也会被创建并保持活动状态,准备立即处理任务。这样做可以减少首次执行任务时的延迟,因为不需要等待线程的创建过程。
要实现这一点,你可以使用prestartCoreThread()
或prestartAllCoreThreads()
方法:
prestartCoreThread()
:尝试启动一个尚未启动的核心线程。如果当前已经有等于核心线程数量的线程处于活动状态,则此方法将不起作用。它返回一个布尔值,指示是否成功启动了一个新的线程。prestartAllCoreThreads()
:启动所有核心线程,即根据设定的核心线程数一次性启动对应数量的线程。此方法会返回实际启动的线程数。
下面是一个简单的例子,演示如何使用这些方法:
java
public class ThreadPoolPreheatExample {
public static void main(String[] args) {
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 5000;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
// 预先启动所有核心线程
int prestartedThreads = executor.prestartAllCoreThreads();
System.out.println("预先启动的核心线程数: " + prestartedThreads);
// 提交一些任务给线程池
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务");
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池(在实际应用中,请确保所有任务已完成)
executor.shutdown();
}
}
在这个例子中,我们首先创建了一个具有5个核心线程和最多10个线程的线程池,并调用prestartAllCoreThreads()
方法来预热线程池,确保所有核心线程都已准备好处理任务。这样,当任务开始提交到线程池时,它们可以立即由已经活跃的核心线程处理,无需等待线程的初始化过程。
给线程池中的线程指定名字🤩
在 Java 中,默认情况下,线程池中创建的线程名称是由 JVM 自动生成的(如 pool-1-thread-1
),但为了方便调试和日志追踪,我们通常希望自定义这些线程的名字。
给线程池中的线程指定名字的方法:
你需要通过实现 ThreadFactory
接口,并传入到线程池中。Java 提供了 java.util.concurrent.ThreadFactory
接口,你可以自定义线程工厂来设置线程名称。
示例:使用自定义 ThreadFactory 设置线程名
java
public class NamedThreadPoolExample {
public static void main(String[] args) {
int corePoolSize = 3;
int maxPoolSize = 5;
long keepAliveTime = 60;
// 自定义线程工厂
ThreadFactory namedThreadFactory = new ThreadFactory() {
private int threadId = 1;
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "MyCustomThread-" + threadId++);
// 可以设置为守护线程或设置优先级等
// t.setDaemon(true);
return t;
}
};
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
namedThreadFactory
);
// 提交任务
for (int i = 0; i < 5; i++) {
final int taskNo = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskNo);
});
}
// 关闭线程池
executor.shutdown();
}
}
输出示例(线程名自定义):
深色版本
MyCustomThread-1 正在执行任务 0
MyCustomThread-2 正在执行任务 1
MyCustomThread-3 正在执行任务 2
MyCustomThread-1 正在执行任务 3
MyCustomThread-2 正在执行任务 4
使用 Executors.defaultThreadFactory()
的变种(可选)
你也可以基于默认的 ThreadFactory
进行包装,比如 Apache Commons 或 Google Guava 提供的工具类(如 ThreadFactoryBuilder
),可以更简洁地命名线程。
例如使用 Guava:
java
import com.google.common.util.concurrent.ThreadFactoryBuilder;
ThreadFactory factory = new ThreadFactoryBuilder()
.setNameFormat("my-pool-%d")
.setDaemon(true)
.build();
ExecutorService executor = Executors.newFixedThreadPool(5, factory);
总结
方法 | 是否推荐 | 说明 |
---|---|---|
实现 ThreadFactory 接口 |
推荐 | 灵活、可控,适合生产环境 |
使用 Guava 的 ThreadFactoryBuilder |
推荐 | 更加简洁,依赖第三方库 |
默认线程工厂 | 不推荐 | 名称不直观,不利于排查问题 |
在平时工作中怎么来制定你的核心线程数和最大线程数😶🌫️
在实际工作中,合理设置线程池的 核心线程数(corePoolSize) 和 最大线程数(maximumPoolSize) 是非常关键的。设置不合理会导致资源浪费、系统响应变慢甚至崩溃。下面是一些常见的思路和方法。
1.根据任务类型选择策略
- CPU密集型任务
-
特点:主要消耗CPU资源,如计算、加密、压缩等。
-
建议:
- 核心线程数 ≈ CPU核数
- 最大线程数 ≈ CPU核数 × 2(视情况)
- 不需要太多线程,避免上下文切换开销过大
java
int corePoolSize = Runtime.getRuntime().availableProcessors();
- IO密集型任务
-
特点:大量等待IO完成(如网络请求、数据库查询、磁盘读写)
-
建议:
- 可以适当增加线程数,因为很多线程处于等待状态
- 核心线程数 = CPU核数 × 2 或更高
- 最大线程数可设为更高的值(如 50~200)
2.结合任务队列分析负载
- 如果任务队列经常积压 → 增加核心线程数或最大线程数
- 如果线程池频繁扩容 → 可能是 corePoolSize 设置太小
- 如果 keepAliveTime 太短,非核心线程刚启动就销毁了 → 浪费资源
3.监控线程池 建议在线上部署时,配合以下手段:
-
使用
ThreadPoolTaskExecutor
(Spring中常用)并暴露指标 -
结合 Prometheus + Grafana 监控:
- 活跃线程数
- 队列大小
- 拒绝任务数
-
动态调整线程池参数(动态线程池)
总结❤️
对Java线程池的理解和使用,是每位Java程序员的必备技能。
这是后端面试常问的问题,建议各位结合自己的项目进行回答,面试官问你的优化方法也可以有更多的思路。如果你看了这篇文章有收获可以点赞+关注+收藏🤩,这是对笔者更新的最大鼓励!如果你有更多方案或者文章中有错漏之处,请在评论区提出帮助笔者勘误,祝你拿到更好的offer!