Java中线程拒绝策略指南

1. 概述

Java 中的Executor Framework试图将任务提交与任务执行分离开来。虽然这种方法很好地抽象了任务执行的细节,但有时我们仍然需要对其进行配置,以实现更优化的执行。

在本文中,我们将了解当线程池无法再接受任何任务时会发生什么。然后,我们将学习如何通过适当应用饱和策略来控制这种特殊情况。 Java 线程拒绝策略指的是当线程池中的所有线程都处于繁忙状态,且队列也已满时,如何处理新提交的任务。这是一种重要的机制,用于防止系统因任务堆积而崩溃。

Java 提供了四种内置的拒绝策略,可以通过 ThreadPoolExecutor 的 RejectedExecutionHandler 属性进行设置:

1. AbortPolicy (默认策略)

  • 当任务被拒绝时,抛出 RejectedExecutionException 异常。
  • 这是默认的拒绝策略,会导致程序直接崩溃。

2. DiscardPolicy

  • 当任务被拒绝时,直接丢弃任务,不进行任何处理。

3. DiscardOldestPolicy

  • 当任务被拒绝时,移除队列中最旧的任务,并尝试重新提交新任务。

4. CallerRunsPolicy

  • 当任务被拒绝时,由提交任务的线程执行该任务。
  • 这可以有效地降低任务提交速率,防止系统过载。

5. 自定义拒绝策略

除了内置的拒绝策略,还可以自定义拒绝策略来满足更复杂的需求。

  • 实现 RejectedExecutionHandler 接口,并重写 rejectedExecution 方法,在该方法中实现自己的拒绝策略逻辑。
  • 例如,可以将被拒绝的任务写入日志文件、保存到数据库,或者使用其他线程池进行处理。

6. 选择合适的拒绝策略

选择合适的拒绝策略取决于具体的应用场景,需要综合考虑以下因素:

  • 任务的重要性: 如果任务非常重要,则应该选择 AbortPolicy 或 DiscardOldestPolicy,避免任务丢失。
  • 系统负载: 如果系统负载很高,则应该选择 DiscardPolicy 或 CallerRunsPolicy,避免系统过载。
  • 任务类型: 如果任务是幂等的,即多次执行不会造成副作用,则可以选择 DiscardPolicy。

2. 重新审视线程池

下图展示了线程执行器服务的内部工作原理:

当我们向执行器提交新任务时会发生以下情况:

  1. 如果其中一个线程可用,它就会处理该任务。
  2. 否则,执行器会将新任务添加到其队列中。
  3. 当线程完成当前任务时,它会从队列中选取另一个任务。

2.1. ThreadPoolExecutor

大多数执行器实现都使用众所周知的ThreadPoolExecutor 作为其基本实现。因此,为了更好地理解任务排队的工作原理,我们应该仔细看看它的构造函数:

java 复制代码
public ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  RejectedExecutionHandler handler
)

2.2. 核心池大小

corePoolSize参数 决定线程池的初始大小。通常,执行器会确保线程池至少包含corePoolSize个线程。

但是,如果我们启用allowCoreThreadTimeOut 参数,则可能会有更少的线程。

2.3. 最大池大小

假设所有核心线程都在忙于执行一些任务。因此,执行器将新任务放入队列,直到有机会稍后处理它们。

当此队列已满时,执行器可以向线程池添加更多线程。maximumPoolSize设置 了线程池可能包含的线程数的上限。**

当这些线程空闲一段时间后,执行器可以将它们从池中移除。因此,池大小可以缩小回其核心大小。

2.4. 排队

如前所述,当所有核心线程都处于繁忙状态时,执行器会将新任务添加到队列中。排队有三种不同的方法

  • 无界队列 :队列可以容纳无限数量的任务。由于此队列永远不会填满,因此执行器会忽略最大大小。固定大小单线程执行器都使用此方法。
  • 有界队列 顾名思义,队列只能容纳有限数量的任务。因此,当有界队列填满时,线程池就会增长。
  • 同步切换 :令人惊讶的是,这个队列不能容纳任何任务!使用这种方法,当且仅当另一侧有另一个线程同时选择同一任务时,我们才能将任务排队缓存线程池执行器在内部使用此方法。

让我们假设在使用有界排队或同步切换时的以下场景:

  • 所有核心线程都处于繁忙状态
  • 内部队列已满
  • 线程池增长到其最大可能大小,并且所有这些线程也都很忙

当有新任务进来时会发生什么?

3. 饱和政策

当所有线程都处于繁忙状态并且内部队列已满时,执行器就会饱和。

一旦达到饱和状态,执行器就可以执行预定义的操作。这些操作称为饱和策略。我们可以通过将RejectedExecutionHandler 的实例传递给其构造函数来修改执行器的饱和策略。* 幸运的是,Java 为此类提供了一些内置实现,每个实现都涵盖了特定的用例。在以下部分中,我们将详细评估这些策略。

3.1. 中止政策

默认策略是中止策略中止策略会导致执行器抛出 RejectedExecutionException

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS, 
  new SynchronousQueue<>(), 
  new ThreadPoolExecutor.AbortPolicy());

executor.execute(() -> waitFor(250));

assertThatThrownBy(() -> executor.execute(() -> System.out.println("Will be rejected")))
  .isInstanceOf(RejectedExecutionException.class);复制

由于第一个任务需要很长时间才能执行,因此执行器拒绝第二个任务。

3.2. 调用者运行策略

此策略不是在另一个线程中异步运行任务,而是让调用者线程执行该任务

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS, 
  new SynchronousQueue<>(), 
  new ThreadPoolExecutor.CallerRunsPolicy());

executor.execute(() -> waitFor(250));

long startTime = System.currentTimeMillis();
executor.execute(() -> waitFor(500));
long blockedDuration = System.currentTimeMillis() - startTime;

assertThat(blockedDuration).isGreaterThanOrEqualTo(500);

提交第一个任务后,执行器无法再接受任何新任务。因此,调用者线程会阻塞,直到第二个任务返回。

调用者运行策略 可以轻松实现一种简单的节流形式。也就是说,慢速消费者可以减慢快速生产者的速度,以控制任务提交流。

3.3. 丢弃策略

丢弃策略 在提交失败时默认丢弃新任务

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS,
  new SynchronousQueue<>(), 
  new ThreadPoolExecutor.DiscardPolicy());

executor.execute(() -> waitFor(100));

BlockingQueue<String> queue = new LinkedBlockingDeque<>();
executor.execute(() -> queue.offer("Discarded Result"));

assertThat(queue.poll(200, MILLISECONDS)).isNull();复制

这里,第二个任务向队列发布一条简单消息。由于它永远没有机会执行,因此即使我们阻塞了一段时间,队列仍然是空的。

3.4. 丢弃最旧策略

丢弃最旧策略 首先从队列头部删除一个任务,然后重新提交新任务

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS, 
  new ArrayBlockingQueue<>(2), 
  new ThreadPoolExecutor.DiscardOldestPolicy());

executor.execute(() -> waitFor(100));

BlockingQueue<String> queue = new LinkedBlockingDeque<>();
executor.execute(() -> queue.offer("First"));
executor.execute(() -> queue.offer("Second"));
executor.execute(() -> queue.offer("Third"));
waitFor(150);

List<String> results = new ArrayList<>();
queue.drainTo(results);

assertThat(results).containsExactlyInAnyOrder("Second", "Third");

这次,我们使用了只能容纳两个任务的有界队列。提交这四个任务时会发生以下情况:

  • 第一个任务占用单线程 100 毫秒
  • 执行器成功将第二和第三个任务加入队列
  • 当第四个任务到达时,丢弃最旧策略会删除最旧的任务,为新任务腾出空间

丢弃最旧策略和优先级队列不能很好地协同工作。 由于优先级队列的头部具有最高优先级,我们可能会丢失最重要的任务

3.5. 自定义策略

也可以通过实现*RejectedExecutionHandler*接口来提供自定义饱和策略:

java 复制代码
class GrowPolicy implements RejectedExecutionHandler {

    private final Lock lock = new ReentrantLock();

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        lock.lock();
        try {
            executor.setMaximumPoolSize(executor.getMaximumPoolSize() + 1);
        } finally {
            lock.unlock();
        }

        executor.submit(r);
    }
}

在此示例中,当执行器饱和时,我们将最大池大小增加一,然后重新提交相同的任务:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS, 
  new ArrayBlockingQueue<>(2), 
  new GrowPolicy());

executor.execute(() -> waitFor(100));

BlockingQueue<String> queue = new LinkedBlockingDeque<>();
executor.execute(() -> queue.offer("First"));
executor.execute(() -> queue.offer("Second"));
executor.execute(() -> queue.offer("Third"));
waitFor(150);

List<String> results = new ArrayList<>();
queue.drainTo(results);

assertThat(results).contains("First", "Second", "Third");

正如预期的那样,所有四个任务均已执行。

3.6. 关机

除了过载的执行器外,饱和策略还适用于所有已关闭的执行器

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS, new LinkedBlockingQueue<>());
executor.shutdownNow();

assertThatThrownBy(() -> executor.execute(() -> {}))
  .isInstanceOf(RejectedExecutionException.class);

对于所有处于关闭状态的执行器来说也是如此:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS, new LinkedBlockingQueue<>());
executor.execute(() -> waitFor(100));
executor.shutdown();

assertThatThrownBy(() -> executor.execute(() -> {}))
  .isInstanceOf(RejectedExecutionException.class);

4. 结论

在本教程中,我们首先快速回顾了 Java 中的线程池。然后,在介绍了饱和执行器之后,我们学习了如何以及何时应用不同的饱和策略。

相关推荐
向前看-3 小时前
验证码机制
前端·后端
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹4 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫5 小时前
泛型(2)
java
超爱吃士力架5 小时前
邀请逻辑
java·linux·后端
南宫生5 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石5 小时前
12/21java基础
java
李小白665 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp5 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea