线程池有哪些拒绝策略?

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 本文介绍了Java线程池的四种拒绝策略:AbortPolicy(默认策略,抛出异常)、CallerRunsPolicy(调用者运行任务)、DiscardPolicy(丢弃任务,不抛异常)和DiscardOldestPolicy(丢弃最旧任务,尝试提交当前任务)。每种策略都有其适用的业务场景,并通过代码示例进行了说明。选择合适的策略取决于具体的应用需求和对任务处理的优先级。

一位有多年开发经验的兄弟最近正在跳槽换工作,虽然同在帝都,好几年都没见面了,周末约着一块小酌一下,聊到面试被问题线程池拒绝策略的问题(木有办法,搞技术的人,聊天不超过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方法来设置。选择哪种策略取决于具体的应用场景和需求。兄弟们,你是如何理解线程池的拒绝策略的呢?欢迎关注【威哥爱编程】一起研究进步。技术路上,一个会走得很累,一群人才能走得更远。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
4月前
|
数据采集 XML JavaScript
C# 中 ScrapySharp 的多线程下载策略
C# 中 ScrapySharp 的多线程下载策略
|
4月前
|
算法 Unix Linux
linux线程调度策略
linux线程调度策略
94 0
|
1月前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
86 2
|
2月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
64 4
|
2月前
|
安全 Java 数据库连接
Python多线程编程:竞争问题的解析与应对策略
Python多线程编程:竞争问题的解析与应对策略
38 0
|
2月前
|
安全 Java 数据库连接
Python多线程编程:竞争问题的解析与应对策略【2】
Python多线程编程:竞争问题的解析与应对策略【2】
34 0
|
4月前
|
安全 Java 程序员
Java编程中实现线程安全的策略
【8月更文挑战第31天】在多线程环境下,保证数据一致性和程序的正确运行是每个程序员的挑战。本文将通过浅显易懂的语言和实际代码示例,带你了解并掌握在Java编程中确保线程安全的几种策略。让我们一起探索如何用同步机制、锁和原子变量等工具来保护我们的数据,就像保护自己的眼睛一样重要。
|
4月前
|
前端开发 JavaScript 大数据
React与Web Workers:开启前端多线程时代的钥匙——深入探索计算密集型任务的优化策略与最佳实践
【8月更文挑战第31天】随着Web应用复杂性的提升,单线程JavaScript已难以胜任高计算量任务。Web Workers通过多线程编程解决了这一问题,使耗时任务独立运行而不阻塞主线程。结合React的组件化与虚拟DOM优势,可将大数据处理等任务交由Web Workers完成,确保UI流畅。最佳实践包括定义清晰接口、加强错误处理及合理评估任务特性。这一结合不仅提升了用户体验,更为前端开发带来多线程时代的全新可能。
103 1
|
4月前
|
监控 负载均衡 算法
线程数突增!领导说再这么写就GC掉我:深入理解与优化策略
【8月更文挑战第29天】在软件开发的世界里,性能优化总是开发者们绕不开的话题。特别是当面对“线程数突增”这样的紧急情况时,更是考验着我们的技术功底和问题解决能力。今天,我们就来深入探讨这一话题,分享一些工作学习中积累的技术干货,帮助大家避免被“GC”(垃圾回收,也常用来幽默地表示“被炒鱿鱼”)的尴尬。
51 2
|
4月前
|
Java
Java线程池如何执行?拒绝策略有哪些?
【8月更文挑战第8天】Java线程池如何执行?拒绝策略有哪些?
71 6