一位有多年开发经验的兄弟最近正在跳槽换工作,虽然同在帝都,好几年都没见面了,周末约着一块小酌一下,聊到面试被问题线程池拒绝策略的问题(木有办法,搞技术的人,聊天不超过10句,准又回到技术上^^)。今天把聊天的内容总结一下,分享给大家。
线程池的拒绝策略是指当线程池中的线程数达到其最大容量,并且队列也满了时,线程池如何处理新提交的任务。在Java中,ThreadPoolExecutor提供了以下四种拒绝策略:
AbortPolicy(默认策略)
:当任务无法被线程池执行时,会抛出一个RejectedExecutionException异常。CallerRunsPolicy
:当任务无法被线程池执行时,会直接在调用者线程中运行这个任务。如果调用者线程正在执行一个任务,则会创建一个新线程来执行被拒绝的任务。DiscardPolicy
:当任务无法被线程池执行时,任务将被丢弃,不抛出异常,也不执行任务。DiscardOldestPolicy
:当任务无法被线程池执行时,线程池会丢弃队列中最旧的任务,然后尝试再次提交当前任务。
下面,V哥对四种拒绝策略再从使用场景、案例代码来详细解释一下,老铁们坐稳扶好,V哥要发车了。
1. AbortPolicy
AbortPolicy是Java线程池中默认的拒绝策略。当线程池达到其最大容量,并且工作队列也满了,无法再接受新的任务时,使用AbortPolicy策略会直接抛出RejectedExecutionException异常。这个异常表明任务因为线程池的资源不足而被拒绝。
业务场景
假设有一个电商平台,需要处理大量的订单处理任务。在高流量的促销活动期间,订单量可能会突然激增,导致线程池中的线程数和队列容量都达到上限。如果继续提交任务,使用AbortPolicy策略,系统会抛出异常,提示开发者或者系统管理员需要关注线程池的资源限制问题。
示例代码
下面是一个使用AbortPolicy策略的线程池示例代码:
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建一个固定大小为5的线程池
int numberOfThreads = 5;
ExecutorService executor = new ThreadPoolExecutor(
numberOfThreads,
numberOfThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()
);
// 提交10个任务到线程池
for (int i = 0; i < 10; i++) {
int finalI = i;
executor.submit(() -> {
System.out.println("Task " + finalI + " is running.");
// 模拟任务执行时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 尝试提交第11个任务,此时线程池和队列已满
try {
executor.submit(() -> {
System.out.println("Task 11 is running.");
});
} catch (RejectedExecutionException e) {
System.out.println("RejectedExecutionException: Task 11 was rejected.");
}
// 关闭线程池
executor.shutdown();
}
}
V哥来解释一下
在这个示例中,我们创建了一个固定大小为5的线程池,并且使用了LinkedBlockingQueue作为工作队列。我们提交了10个任务,每个任务简单地打印一条消息并休眠1秒。
当尝试提交第11个任务时,由于线程池中的线程数和队列都已满,任务无法被执行。此时,线程池使用默认的AbortPolicy策略,抛出RejectedExecutionException异常。这个异常可以通过捕获来处理,例如在示例中,我们通过catch块捕获了这个异常,并打印了一条消息。
这种策略适合于那些不能容忍任务被丢弃或延迟执行的业务场景,因为它会立即通知调用者任务被拒绝,从而可以采取相应的措施,比如增加线程池大小、优化任务执行效率或者通知用户等待。
2. CallerRunsPolicy
CallerRunsPolicy是Java线程池中的一种拒绝策略,当线程池中的线程数达到其最大容量,并且工作队列也满了,无法再接受新的任务时,使用CallerRunsPolicy策略会将任务交由调用者线程(即提交任务的线程)来执行。如果调用者线程已经在执行一个任务,则会创建一个新线程来执行被拒绝的任务。
业务场景
假设有一个在线视频处理服务,用户上传视频后,服务需要对视频进行转码、压缩等处理。在某些情况下,如果视频处理任务过多,线程池可能会达到其最大容量,此时使用CallerRunsPolicy策略可以保证任务不会被丢弃,而是在调用者线程中执行,从而确保所有上传的视频都能得到处理。
示例代码
下面是一个使用CallerRunsPolicy策略的线程池示例代码:
import java.util.concurrent.*;
public class CallerRunsPolicyDemo {
public static void main(String[] args) {
// 创建一个固定大小为2的线程池
int numberOfThreads = 2;
ExecutorService executor = new ThreadPoolExecutor(
numberOfThreads,
numberOfThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
new ThreadPoolExecutor.CallerRunsPolicy() // 设置拒绝策略为CallerRunsPolicy
);
// 提交4个任务到线程池
for (int i = 0; i < 4; i++) {
int finalI = i;
executor.submit(() -> {
System.out.println("Task " + finalI + " is running.");
// 模拟任务执行时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 尝试提交第5个任务,此时线程池和队列已满
executor.submit(() -> {
System.out.println("Task 5 is running in the caller thread.");
});
// 关闭线程池
executor.shutdown();
}
}
V哥来解释一下
在这个示例中,我们创建了一个固定大小为2的线程池,并且使用了CallerRunsPolicy作为拒绝策略。我们提交了4个任务,每个任务简单地打印一条消息并休眠1秒。
当尝试提交第5个任务时,由于线程池中的线程数和队列都已满,任务无法被线程池中的线程执行。此时,根据CallerRunsPolicy策略,任务将由提交任务的线程(即main线程)来执行。因此,你会看到"Task 5 is running in the caller thread."这条消息被打印出来。
这种策略适合于那些可以容忍任务在调用者线程中执行的业务场景,它允许任务继续执行,而不会因为线程池资源不足而被丢弃。但是,需要注意的是,如果调用者线程本身就很忙,或者任务执行时间很长,这可能会导致调用者线程被阻塞,从而影响系统的响应性。
3. DiscardPolicy
DiscardPolicy是Java线程池中的一种拒绝策略,它在任务无法被线程池执行时,会直接丢弃该任务,不执行也不抛出任何异常。
业务场景
假设有一个日志收集系统,该系统负责收集来自多个服务的日志信息。由于日志信息量巨大,线程池可能很快就会达到其最大容量,并且工作队列也会被填满。在这种情况下,使用DiscardPolicy策略可以避免系统因为尝试处理大量日志信息而变得不稳定或崩溃。对于日志信息来说,丢弃一些信息可能是可接受的,因为它们可以稍后通过其他方式重新收集或恢复。
示例代码
下面是一个使用DiscardPolicy策略的线程池示例代码:
import java.util.concurrent.*;
public class DiscardPolicyDemo {
public static void main(String[] args) {
// 创建一个固定大小为2的线程池
int numberOfThreads = 2;
ExecutorService executor = new ThreadPoolExecutor(
numberOfThreads,
numberOfThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(2), // 限制队列大小为2
new ThreadPoolExecutor.DiscardPolicy() // 设置拒绝策略为DiscardPolicy
);
// 提交5个任务到线程池
for (int i = 0; i < 5; i++) {
int finalI = i;
executor.submit(() -> {
System.out.println("Task " + finalI + " is running.");
// 模拟任务执行时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 由于线程池和队列已满,提交的第3个任务将被丢弃,不打印任何消息
}
}
V哥来解释一下
在这个示例中,我们创建了一个固定大小为2的线程池,并且设置了工作队列的大小为2。这意味着线程池最多只能同时执行2个任务,并且队列中最多只能有2个等待执行的任务。
我们提交了5个任务,每个任务简单地打印一条消息并休眠1秒。当提交第3个任务时,线程池的线程数和队列都已满,根据DiscardPolicy策略,这个任务将被丢弃,不会有任何异常抛出,也不会有消息打印出来。
这种策略适合于那些对任务执行的及时性要求不高,或者任务可以被安全丢弃的业务场景。例如,在日志收集、数据监控、非关键性消息处理等场景中,使用DiscardPolicy可以避免系统因为处理大量任务而变得不稳定。然而,需要注意的是,使用这种策略可能会导致数据丢失或任务未被执行,因此在决定使用DiscardPolicy之前,需要仔细考虑业务需求和潜在的影响。
4. DiscardOldestPolicy
DiscardOldestPolicy是Java线程池中的一种拒绝策略,当线程池中的线程数达到其最大容量,并且工作队列也满了,无法再接受新的任务时,使用DiscardOldestPolicy策略会从队列中丢弃最旧的任务(即队列头部的任务),然后尝试再次提交当前任务。
业务场景
假设有一个实时数据处理系统,该系统需要处理来自传感器的实时数据流。在这种情况下,系统可能更倾向于处理最新的数据,而不是旧的数据,因为最新的数据对于分析和决策更为重要。使用DiscardOldestPolicy策略,系统可以丢弃旧的数据任务,以确保有足够的资源来处理最新的数据。
示例代码
下面是一个使用DiscardOldestPolicy策略的线程池示例代码:
import java.util.concurrent.*;
public class DiscardOldestPolicyDemo {
public static void main(String[] args) {
// 创建一个固定大小为2的线程池
int numberOfThreads = 2;
ExecutorService executor = new ThreadPoolExecutor(
numberOfThreads,
numberOfThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(2), // 限制队列大小为2
new ThreadPoolExecutor.DiscardOldestPolicy() // 设置拒绝策略为DiscardOldestPolicy
);
// 提交5个任务到线程池
for (int i = 0; i < 5; i++) {
final int taskNumber = i;
executor.submit(() -> {
System.out.println("Task " + taskNumber + " is running.");
// 模拟任务执行时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 等待所有任务执行完毕
executor.shutdown();
while (!executor.isTerminated()) {
// 等待线程池关闭
}
System.out.println("All tasks have been processed.");
}
}
V哥来解释一下
在这个示例中,我们创建了一个固定大小为2的线程池,并且设置了工作队列的大小为2。这意味着线程池最多只能同时执行2个任务,并且队列中最多只能有2个等待执行的任务。
我们提交了5个任务,每个任务简单地打印一条消息并休眠1秒。当提交第3个任务时,线程池的线程数和队列都已满。根据DiscardOldestPolicy策略,队列中的第一个任务(即任务0)将被丢弃,然后尝试再次提交当前任务(任务3)。这样,任务1和任务2将被执行,任务3将替换任务0的位置并被执行,而任务4和任务5将依次进入队列并被执行。
这种策略适合于那些对最新数据或任务更为敏感的业务场景,例如实时数据处理、股票交易系统、在线游戏服务器等。在这些场景中,丢弃旧的任务以保证新任务的执行可能是一个合理的选择。然而,需要注意的是,使用这种策略可能会导致数据丢失或旧任务未被执行,因此在决定使用DiscardOldestPolicy之前,需要仔细考虑业务需求和潜在的影响。
最后
这些策略可以通过ThreadPoolExecutor的构造函数或setRejectedExecutionHandler方法来设置。选择哪种策略取决于具体的应用场景和需求。兄弟们,你是如何理解线程池的拒绝策略的呢?欢迎关注【威哥爱编程】一起研究进步。技术路上,一个会走得很累,一群人才能走得更远。