线程池有哪些拒绝策略?

一位有多年开发经验的兄弟最近正在跳槽换工作,虽然同在帝都,好几年都没见面了,周末约着一块小酌一下,聊到面试被问题线程池拒绝策略的问题(木有办法,搞技术的人,聊天不超过10句,准又回到技术上^^)。今天把聊天的内容总结一下,分享给大家。

在开始前,推荐一款程序员都应该知道的好物——JNPF低代码开发

应用地址:https://www.jnpfsoft.com

这是一个基于 Java Boot/.Net Core 构建的简单、跨平台快速开发框架。前后端封装了上千个常用类,方便扩展;采用微服务、前后端分离架构,集成了代码生成器,支持前后端业务代码生成,满足快速开发;框架集成了表单、报表、图表、大屏等各种常用的 Demo 方便直接使用;后端框架支持 Vue2、Vue3,平台即可私有化部署,也支持 K8S 部署。

在 JNPF 中,至少包含表单建模、流程设计、报表可视化、代码生成器、系统管理、前端 UI 等组件,这种情况下我们避免了重复造轮子,已内置大量的成熟组件,选择合适的组件进行集成或二次开发复杂功能,即可自主开发一个属于自己的应用系统。

线程池的拒绝策略是指当线程池中的线程数达到其最大容量,并且队列也满了时,线程池如何处理新提交的任务。在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方法来设置。选择哪种策略取决于具体的应用场景和需求。

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值