并发编程 | 线程池的拒绝策略

4种默认拒绝策略

线程池的拒绝策略,是指是指当任务添加到线程池中被拒绝,而采取的处理措施。我们还是复习下利用ThreadPoolExecutor手动创建线程池的构造方法:

public ThreadPoolExecutor(int corePoolSize,
           int maximumPoolSize,
           long keepAliveTime,
           TimeUnit unit,
           BlockingQueue<Runnable> workQueue                    
           ThreadFactory threadFactory,
           RejectedExecutionHandler handler
   ) 

其中参数handler就是用来控制拒绝策略的。当任务添加到线程池中之所以会被拒绝,主要有2种原因:

  1. 线程池异常关闭。
  2. 任务数量超过线程池的最大限制。

线程池包括4种默认的拒绝策略,它们分别是:

  • AbortPolicy
  • CallerRunsPolicy
  • DiscardOldestPolicy
  • DiscardPolicy。

下面分别介绍

AbortPolicy拒绝策略

线程池默认的处理策略就是是AbortPolicy,任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务;并抛出RejectedExecutionException异常。用代码说话:

class MyRunnabless implements Runnable{
    private int i;
    MyRunnabless(int i){
        this.i = i;
    }

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " 线程"  + Thread.currentThread().getName() + " 执行任务" + i);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadPoolRejectPolicyTest{
    public void testAbortPolicy(){
        ExecutorService executorService = new ThreadPoolExecutor(2, 4, 2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2));
        for (int i=0; i<10; i++){
            executorService.submit(new MyRunnabless(i));
        }
    }
}

//线程池拒绝策略测试
public class ThreadPoolRejectPolicyExample {
    public static void main(String[] args) {
        ThreadPoolRejectPolicyTest threadPoolRejectPolicyTest = new ThreadPoolRejectPolicyTest();
        threadPoolRejectPolicyTest.testAbortPolicy();
    }
}

//输出
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@179d3b25[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@1a6c5a9e[Wrapped task = thread.threadpool.MyRunnabless@37bba400]] rejected from java.util.concurrent.ThreadPoolExecutor@254989ff[Running, pool size = 4, active threads = 4, queued tasks = 2, completed tasks = 0]
    at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2055)
    at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825)
    at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1355)
    at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:118)
    at thread.threadpool.ThreadPoolRejectPolicyTest.testAbortPolicy(ThreadPoolRejectPolicyExample.java:33)
    at thread.threadpool.ThreadPoolRejectPolicyExample.main(ThreadPoolRejectPolicyExample.java:42)
22/06/25-21:59:13 线程pool-1-thread-2 执行任务1
22/06/25-21:59:13 线程pool-1-thread-4 执行任务5
22/06/25-21:59:13 线程pool-1-thread-1 执行任务0
22/06/25-21:59:13 线程pool-1-thread-3 执行任务4
22/06/25-21:59:15 线程pool-1-thread-3 执行任务3
22/06/25-21:59:15 线程pool-1-thread-4 执行任务2

这个程序在前面已经分析过了,后面提交的4个任务都被拒绝了,并且抛出了RejectedExecutionException异常,因为创建线程池时没有指定拒绝策略,则默认是AbortPolicy拒绝策略。

DiscardPolicy拒绝策略

当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务。但是和AbortPolicy策略不同的是DiscardPolicy 策略不会抛出异常,还是用代码说话,我们只需关注标红的那行代码即可,代码其它部分和前面例子代码一样:

class MyRunnabless implements Runnable{
    private int i;
    MyRunnabless(int i){
        this.i = i;
    }

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " 线程"  + Thread.currentThread().getName() + " 执行任务" + i);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadPoolRejectPolicyTest{
    //DiscardPolicy策略
    public void testDiscardPolicy(){
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 2, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2));
        threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
        for (int i=0; i<10; i++){
            threadPoolExecutor.submit(new MyRunnabless(i));
        }
    }
}

//线程池拒绝策略测试
public class ThreadPoolRejectPolicyExample {
    public static void main(String[] args) {
        ThreadPoolRejectPolicyTest threadPoolRejectPolicyTest = new ThreadPoolRejectPolicyTest();
        threadPoolRejectPolicyTest.testDiscardPolicy();
    }
}

//输出
22/06/25-22:50:02 线程pool-1-thread-3 执行任务4
22/06/25-22:50:02 线程pool-1-thread-1 执行任务0
22/06/25-22:50:02 线程pool-1-thread-2 执行任务1
22/06/25-22:50:02 线程pool-1-thread-4 执行任务5
22/06/25-22:50:04 线程pool-1-thread-4 执行任务3
22/06/25-22:50:04 线程pool-1-thread-2 执行任务2  

根据运行结果可以看出,在线程池设置了拒绝策略为DiscardPolicy后,当最大线程数目的线程都在执行任务并且任务队列已满,再有任务提交时被丢弃了,但是不会抛出异常。这里需要注意的是:丢弃的是后面提交的任务6,7,8,9。在介绍另外一个拒绝策略DiscardOldestPolicy时会和这里作对比。

DiscardOldestPolicy拒绝策略

当任务添加到线程池中被拒绝时,线程池会丢弃等待队列中最旧的未处理任务,然后将新的任务添加到等待队列中。如下代码,还是主要关注代码中标红的部分:

class MyRunnabless implements Runnable{
    private int i;
    MyRunnabless(int i){
        this.i = i;
    }

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " 线程"  + Thread.currentThread().getName() + " 执行任务" + i);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadPoolRejectPolicyTest{
    //DiscardOldestPolicy策略
    public void testDiscardOldestPolicy(){
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 2, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2));
        threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
        for (int i=0; i<10; i++){
            threadPoolExecutor.submit(new MyRunnabless(i));
        }
    }
}

//线程池拒绝策略测试
public class ThreadPoolRejectPolicyExample {
    public static void main(String[] args) {
        ThreadPoolRejectPolicyTest threadPoolRejectPolicyTest = new ThreadPoolRejectPolicyTest();
        threadPoolRejectPolicyTest.testDiscardOldestPolicy();
    }
}

//输出
22/06/25-22:52:50 线程pool-1-thread-2 执行任务1
22/06/25-22:52:50 线程pool-1-thread-3 执行任务4
22/06/25-22:52:50 线程pool-1-thread-4 执行任务5
22/06/25-22:52:50 线程pool-1-thread-1 执行任务0
22/06/25-22:52:52 线程pool-1-thread-2 执行任务8
22/06/25-22:52:52 线程pool-1-thread-1 执行任务9       

可以看出,最后执行的两个任务是最后提交的任务8,9,整个流程是这样的:最先提交的任务0,1被创建的2个核心线程去执行;然后提交任务2,3时,放到任务队列中;然后再提交任务4,5,创建2个非核心线程去执行,这时任务队列已满,所有线程在执行相应任务。

此时再提交任务6,根据设置的拒绝策略DiscardOldestPolicy,将位于队列中最久的任务2从队列中放弃,任务6入队,队列长度还是保持为2;后面再提交任务7,将任务3从队列中放弃,任务7入队;同理,随着新任务的提交,任务6,7也会被放弃,最后队列中只剩下最新提交的任务8,9,最后被执行。

CallerRunsPolicy 拒绝策略

相对其它拒绝策略,CallerRunsPolicy策略比较完善:有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做的好处有2点:

  1. 新提交的任务不会被丢弃,这样也就不会造成业务损失。

  2. 由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。

class MyRunnabless implements Runnable{
    private int i;
    MyRunnabless(int i){
        this.i = i;
    }

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " 线程"  + Thread.currentThread().getName() + " 执行任务" + i);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadPoolRejectPolicyTest{
    //CallerRunsPolicy策略
    public void testCallerRunsPolicy(){
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 2, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2));
        threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        for (int i=0; i<10; i++){
            threadPoolExecutor.submit(new MyRunnabless(i));
        }
    }
}

//线程池拒绝策略测试
public class ThreadPoolRejectPolicyExample {
    public static void main(String[] args) {
        ThreadPoolRejectPolicyTest threadPoolRejectPolicyTest = new ThreadPoolRejectPolicyTest();
        threadPoolRejectPolicyTest.testCallerRunsPolicy();
    }
}

//输出
22/06/25-23:12:48 线程pool-1-thread-4 执行任务5
22/06/25-23:12:48 线程pool-1-thread-2 执行任务1
22/06/25-23:12:48 线程pool-1-thread-1 执行任务0
22/06/25-23:12:48 线程main 执行任务6
22/06/25-23:12:48 线程pool-1-thread-3 执行任务4
22/06/25-23:12:50 线程pool-1-thread-2 执行任务3
22/06/25-23:12:50 线程main 执行任务7
22/06/25-23:12:50 线程pool-1-thread-3 执行任务2
22/06/25-23:12:52 线程pool-1-thread-3 执行任务8
22/06/25-23:12:52 线程pool-1-thread-2 执行任务9

通过运行结果可以看出,所有提交的任务都会被执行,但是线程6,7在提交时候是由主线程main去执行的,主线程在执行完任务7后,线程池的线程已经有空闲的了,所以任务8,9仍由线程池的线程去执行。

不同策略的适用场景

AbortPolicy

场景: 当你需要确保每一个提交的任务都能被执行,或者希望在任务被拒绝时立刻得到反馈。这种策略适用于任务失败后需要立即处理异常或重试的场合。

举例: 比如处理金融交易、订单系统等,任务丢失可能导致严重后果的系统。在这种场景下,如果任务被拒绝,需要及时抛出异常,让调用者立即处理。

CallerRunsPolicy

场景: 当你希望系统在高负载下能够自动减缓任务提交速度,避免过载,同时又不希望直接丢弃任务。这种策略适用于希望将任务执行压力回退到调用方的场合。

举例: 比如后台批处理任务系统,如果线程池繁忙,则让任务提交方自己执行一些任务,以此降低线程池的压力。这样即使线程池繁忙,任务也能得到执行,虽然可能会导致执行时间变长。

DiscardOldestPolicy

场景: 当你优先处理最新的任务,认为较早的任务如果没有执行就可以丢弃。这种策略适用于对实时性要求较高的场合,而不在意较早的任务可能被丢弃。

举例: 比如日志系统,最新的日志需要优先处理,而较早的日志如果没有处理就可以忽略。在这种情况下,丢弃最旧的日志条目,让最新的日志条目能够得到处理。

DiscardPolicy

场景: 当任务丢失不会对系统造成影响,或者系统可以容忍任务丢失时使用这种策略。这种策略适用于非关键性任务或系统负载过高时不需要进一步处理的场景。

举例: 比如统计数据的采集系统,每秒钟产生的数据量很大,但不是每一条数据都必须被处理。在这种场景下,如果线程池繁忙,可以直接丢弃一些任务,不会影响整体系统的稳定性和性能。

自定义拒绝策略

自定义拒绝策略可以让你在线程池任务被拒绝时,定义自己的处理逻辑。这样你就可以根据具体的业务需求来决定当线程池无法处理新的任务时应该做什么。

自定义拒绝策略的步骤:

  1. 实现RejectedExecutionHandler接口: 要自定义拒绝策略,你需要实现RejectedExecutionHandler接口。这个接口有一个方法rejectedExecution(Runnable r, ThreadPoolExecutor executor),当任务被拒绝时,这个方法会被调用。

  2. 定义自己的处理逻辑: 在rejectedExecution方法中,你可以定义自己的处理逻辑。例如,可以选择记录日志、将任务保存到数据库、重试执行任务,甚至通知管理员等。

  3. 将自定义策略应用到线程池: 创建线程池时,将自定义的拒绝策略传递给线程池的构造方法。

public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 打印日志,告知任务被拒绝
        System.out.println("任务被拒绝: " + r.toString());
        // 抛出异常或执行其他逻辑
        throw new RejectedExecutionException("任务 " + r.toString() + " 被线程池拒绝");
    }

    public static void main(String[] args) {
        // 创建线程池,核心线程数和最大线程数都为1,阻塞队列大小为1
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1, 1, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(1), // 阻塞队列大小为1
                new CustomRejectedExecutionHandler() // 使用自定义的拒绝策略
        );

        // 提交3个任务,第三个任务会被拒绝
        for (int i = 1; i <= 3; i++) {
            int taskId = i;
            executor.submit(() -> {
                try {
                    // 每个任务执行3秒
                    Thread.sleep(3000);
                    System.out.println("任务 " + taskId + " 执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}


//输出
任务被拒绝: java.util.concurrent.FutureTask@7d6f77cc[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@123772c4[Wrapped task = ex_02_thread.CustomRejectedExecutionHandler$$Lambda$14/0x000000080115ec40@2d363fb3]]
Exception in thread "main" java.util.concurrent.RejectedExecutionException: 任务 java.util.concurrent.FutureTask@7d6f77cc[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@123772c4[Wrapped task = ex_02_thread.CustomRejectedExecutionHandler$$Lambda$14/0x000000080115ec40@2d363fb3]] 被线程池拒绝
	at ex_02_thread.CustomRejectedExecutionHandler.rejectedExecution(CustomRejectedExecutionHandler.java:14)
	at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825)
	at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1355)
	at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:118)
	at ex_02_thread.CustomRejectedExecutionHandler.main(CustomRejectedExecutionHandler.java:28)
任务 1 执行完毕
任务 2 执行完毕

可以看出,当提交超过线程池最大处理能力的任务时,第三个任务会被拒绝,并执行了自定义的拒绝策略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值