ScheduledExecutorService 异常处理

75
我使用ScheduledExecutorService定期执行一个方法。

代码:

ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture<?> handle =
        scheduler.scheduleWithFixedDelay(new Runnable() {
             public void run() { 
                 //Do business logic, may Exception occurs
             }
        }, 1, 10, TimeUnit.SECONDS);

我的问题:

如果run()抛出异常,如何继续调度程序?我应该在方法run()中尝试捕获所有异常吗?还是有任何内置回调方法来处理异常?谢谢!

9个回答

152

简短概述

任何从您的run方法中逃逸的异常会无预警地停止所有进一步的工作

始终在您的run方法内使用try-catch。如果您想要继续计划活动,请尝试恢复。

@Override
public void run ()
{
    try {
        doChore();
    } catch ( Exception e ) { 
        logger.error( "Caught exception in ScheduledExecutorService. StackTrace:\n" + t.getStackTrace() );
    }
}

问题

这个问题涉及到使用ScheduledExecutorService时的一个关键技巧:任何抛出到执行器的异常或错误都会导致执行器停止。不再有对Runnable的调用,也没有更多的工作。这种工作停顿是悄无声息的,你不会得到通知。这篇恶意语言的blog posting生动地叙述了学习这种行为的艰难之路。

解决方案

answer by yegor256answer by arun_suresh的答案似乎基本上是正确的。这些答案存在两个问题:

  • 捕获错误以及异常
  • 有点复杂

错误异常?

在Java中,我们通常只捕获exceptions,而不是errors。但是,在ScheduledExecutorService的特殊情况下,无法捕获任何一个都意味着工作停滞。因此,您可能希望同时捕获两者。我并不100%确定这一点,因为我不完全了解捕获所有错误的影响。如果需要,请纠正我。

捕获错误和异常的原因之一可能涉及在任务中使用库。请参见jannis的评论

捕获异常和错误的一种方法是捕获它们的超类Throwable,以下是一个示例。

} catch ( Throwable t ) {

…而不是…

} catch ( Exception e ) {

最简单的方法:只需添加Try-Catch

但是两种答案都有点复杂。仅供参考,我将展示最简单的解决方案:

始终将您的Runnable代码包装在Try-Catch中,以捕获任何和所有异常和错误。

Lambda语法

使用lambda(在Java 8及更高版本中)。

final Runnable someChoreRunnable = () -> {
    try {
        doChore();
    } catch ( Throwable t ) {  // Catch Throwable rather than Exception (a subclass).
        logger.error( "Caught exception in ScheduledExecutorService. StackTrace:\n" + t.getStackTrace() );
    }
};

老式语法

在使用lambda之前的老式方式。

final Runnable someChoreRunnable = new Runnable()
{
    @Override
    public void run ()
    {
        try {
            doChore();
        } catch ( Throwable t ) {  // Catch Throwable rather than Exception (a subclass).
            logger.error( "Caught exception in ScheduledExecutorService. StackTrace:\n" + t.getStackTrace() );
        }
    }
};

在每个Runnable/Callable中

无论是 ScheduledExecutorService,对于任何一个 Runnablerun 方法,我觉得始终使用通用的 try-catch( Exception† e ) 是明智的。对于任何一个 Callablecall 方法同样适用。


完整示例代码

在实际工作中,我通常会单独定义Runnable而不是嵌套。但这使得示例代码更加整洁。

package com.basilbourque.example;

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 *  Demo `ScheduledExecutorService`
 */
public class App {
    public static void main ( String[] args ) {
        App app = new App();
        app.doIt();
    }

    private void doIt () {

        // Demonstrate a working scheduled executor service.
        // Run, and watch the console for 20 seconds.
        System.out.println( "BASIL - Start." );

        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        ScheduledFuture < ? > handle =
                scheduler.scheduleWithFixedDelay( new Runnable() {
                    public void run () {
                        try {
                            // doChore ;   // Do business logic.
                            System.out.println( "Now: " + ZonedDateTime.now( ZoneId.systemDefault() ) );  // Report current moment.
                        } catch ( Exception e ) {
                            // … handle exception/error. Trap any unexpected exception here rather to stop it reaching and shutting-down the scheduled executor service.
                            // logger.error( "Caught exception in ScheduledExecutorService. StackTrace:\n" + e.getStackTrace() );
                        }   // End of try-catch.
                    }   // End of `run` method.
                } , 0 , 2 , TimeUnit.SECONDS );


        // Wait a long moment, for background thread to do some work.
        try {
            Thread.sleep( TimeUnit.SECONDS.toMillis( 20 ) );
        } catch ( InterruptedException e ) {
            e.printStackTrace();
        }

        // Time is up. Kill the executor service and its thread pool.
        scheduler.shutdown();

        System.out.println( "BASIL - Done." );

    }
}

运行时。

现在时间: 2018-04-10T16:46:01.423286-07:00[美国/洛杉矶]

现在时间: 2018-04-10T16:46:03.449178-07:00[美国/洛杉矶]

现在时间: 2018-04-10T16:46:05.450107-07:00[美国/洛杉矶]

现在时间: 2018-04-10T16:46:07.450586-07:00[美国/洛杉矶]

现在时间: 2018-04-10T16:46:09.456076-07:00[美国/洛杉矶]

现在时间: 2018-04-10T16:46:11.456872-07:00[美国/洛杉矶]

现在时间: 2018-04-10T16:46:13.461944-07:00[美国/洛杉矶]

现在时间: 2018-04-10T16:46:15.463837-07:00[美国/洛杉矶]

现在时间: 2018-04-10T16:46:17.469218-07:00[美国/洛杉矶]

现在时间: 2018-04-10T16:46:19.473935-07:00[美国/洛杉矶]

另一个例子

这是另一个例子。我们的任务需要运行大约20次,每5秒运行一次,持续1分钟。但在第五次运行时,我们会抛出异常。

public class App2
{
    public static void main ( String[] args )
    {
        ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
        final AtomicInteger counter = new AtomicInteger( 0 );
        Runnable task = ( ) -> {
            int c = counter.incrementAndGet();
            if ( c > 4 )
            {
                System.out.println( "THROWING EXCEPTION at " + Instant.now() );
                throw new IllegalStateException( "Bogus exception. c = " + c + ". " + Instant.now() ); // Notice how this exception is silently swallowed by the scheduled executor service, while causing a work stoppage.
            }
            System.out.println( "Task running. c = " + c + ". " + Instant.now() );
        };
        ses.scheduleAtFixedRate( task , 0 , 5 , TimeUnit.SECONDS );

        try { Thread.sleep( Duration.ofMinutes( 1 ).toMillis() ); }catch ( InterruptedException e ) { e.printStackTrace(); }
        System.out.println( "Main thread done sleeping. " + Instant.now() );

        ses.shutdown();
        try { ses.awaitTermination( 1 , TimeUnit.MINUTES ); }catch ( InterruptedException e ) { e.printStackTrace(); }
    }
}

运行时。

Task running. c = 1. 2021-10-14T20:09:16.317995Z
Task running. c = 2. 2021-10-14T20:09:21.321536Z
Task running. c = 3. 2021-10-14T20:09:26.318642Z
Task running. c = 4. 2021-10-14T20:09:31.318320Z
THROWING EXCEPTION at 2021-10-14T20:09:36.321458Z
Main thread done sleeping. 2021-10-14T20:10:16.320430Z

注意:
  • 定时执行服务会默默地吞噬异常。
  • 工作停顿发生。我们的任务不再被调度执行。同样是一个静默的问题。

因此,当您的任务抛出异常时,您将获得最糟糕的结果:没有解释的静默工作停顿。

解决方案如上所述:始终在您的run方法内使用try-catch


或者使用Throwable而不是Exception来捕获Error对象。


3
@给踩票者...请在投票时附上评论批评。 - Basil Bourque
5
那篇博客文章让我开心不已。 - Daniel
3
为了提供一个实际案例,说明为什么要捕获Throwable而不是Exception:我最近遇到了一个第三方库的问题,这个库对服务进行定期健康检查。该库在内部使用了ScheduledExecutorService。我一直从健康检查中收到超时的错误。我阅读了相关的博客文章后,发现调度程序可能会吞噬错误。因此,我添加了一个try-catch块来捕获Exception,但它没有起作用。经过一周左右的调查后,我发现原来是一个依赖项的问题导致了一个NoSuchMethodError错误,而这是一个Error类型。 - jannis
5
(A) 处理异常的方法通常比简单让应用程序崩溃更好。(B) 在这种情况下,你忽略了重点:你的应用程序不会崩溃! 被抛出的异常被定时执行器服务所吞噬,这停止了进一步执行任务的调度。因此,你会得到最糟糕的结果:静默工作停滞,没有解释。因此,我的建议是正确的:“始终在你的run方法中使用try-catch。”(C) 我添加了另一个代码示例到答案中,以演示抑制抛出的异常。 - Basil Bourque
4
@JeffreyBlattman,你好像认为我建议用空的大括号catch (Exception e) {}来处理异常。我从未这样说过。我说的是,从计划执行器服务中冒出的异常不会导致应用程序崩溃。因此,在这种情况下,“让应用程序崩溃”这种方法行不通。我甚至给了你一个代码示例来证明我的观点。试一试;我已经尝试并在发布之前编写了它。试试看——你的应用程序不会**崩溃 - Basil Bourque
显示剩余6条评论

33

您应该像这样使用由scheduler.scheduleWithFixedDelay(...)返回的ScheduledFuture对象:

ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture<?> handle =
        scheduler.scheduleWithFixedDelay(new Runnable() {
             public void run() { 
                 throw new RuntimeException("foo");
             }
        }, 1, 10, TimeUnit.SECONDS);

// Create and Start an exception handler thread
// pass the "handle" object to the thread
// Inside the handler thread do :
....
try {
  handle.get();
} catch (ExecutionException e) {
  Exception rootException = e.getCause();
}

11
我不明白。在我看来,你的try-catch会立即运行一次。但是你的定时任务将每隔10秒执行一次。你的代码如何捕获后续执行中抛出的任何异常? - Basil Bourque
1
@BasilBourque 没有后续执行。一旦抛出异常,作业将被卡住。此代码仅确保它不会悄无声息地发生,因此如果发生了,您就知道何时以及为什么会发生。让执行继续运行的唯一方法是用try/catch包围其中的所有内容。 - kaqqao
3
我猜这个方法可以奏效,但需要额外开启一个线程来阻塞在 handle.get() 上。如果可以在执行器服务的线程内处理会更好。 - Shannon
10
@Basil是正确的。这种方法毫无意义。很遗憾,目前仍然有人接受这个答案。它只会让用户感到困惑,比如这个问题:https://stackoverflow.com/q/63366045。任何读者:请继续阅读下一个帖子,那个目前得票最多的。 - BalusC

8

虽然这是一个旧问题,但是被接受的答案没有提供解释,并且给出了一个糟糕的示例,而最受欢迎的答案在某些方面是正确的,但最终鼓励你在每个Runnable.run()方法中添加catch异常。
我不同意,因为:

  • 这不整洁:任务捕获自己的异常不是标准做法。
  • 这不够健壮:新的Runnable子类可能会忘记执行异常捕获和故障转移。
  • 这破坏了任务所推广的低耦合性,因为它将任务与处理任务结果的方式耦合在一起。
  • 这混淆了责任:处理异常或将异常通知调用者并不是任务的责任。任务是要执行的内容。

我认为异常传播应该由ExecutorService框架来执行,实际上它提供了这个功能。
此外,尝试通过缩短ExecutorService工作方式来过于聪明也不是一个好主意:框架可能会发生变化,并且您希望以标准方式使用它。
最后,让ExecutorService框架执行其工作不意味着必须停止后续调用任务。
如果计划任务遇到问题,那么调用者有责任根据问题原因重新安排或不重新安排任务。
每个层级都有其责任。保持这些责任能使代码清晰且易于维护。


ScheduledFuture.get():捕获任务中发生的异常和错误的正确API

ScheduledExecutorService.scheduleWithFixedDelay()/scheduleAtFixRate()在规范中声明:

如果任务的任何执行遇到异常,则会抑制随后的执行。否则,任务仅通过执行器的取消或终止来终止。

它意味着ScheduledFuture.get()不会在每次计划的调用时返回,而是仅在任务的最后一次调用时返回,即任务取消:由ScheduledFuture.cancel()引起或在任务中抛出异常。
因此,使用ScheduledFuture.get()处理ScheduledFuture返回值以捕获异常是正确的:

  try {
    future.get();

  } catch (InterruptedException e) {
    // ... to handle
  } catch (ExecutionException e) {
    // ... and unwrap the exception OR the error that caused the issue
    Throwable cause = e.getCause();       
  }

默认行为示例:如果任务执行中出现问题,则停止调度

它执行一个任务,第三次执行时抛出异常并终止调度。在某些情况下,我们希望如此。

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ScheduledExecutorServiceWithException {

  public static void main(String[] args) {
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

    // variable used to thrown an error at the 3rd task invocation
    AtomicInteger countBeforeError = new AtomicInteger(3);

    // boolean allowing to leave the client to halt the scheduling task or not after a failure
    Future<?> futureA = executor
        .scheduleWithFixedDelay(new MyRunnable(countBeforeError), 1, 2, TimeUnit.SECONDS);
    try {
      System.out.println("before get()");
      futureA.get(); // will return only if canceled
      System.out.println("after get()");
    } catch (InterruptedException e) {
      // handle that : halt or no
    } catch (ExecutionException e) {
      System.out.println("exception caught :" + e.getCause());
    }

    // shutdown the executorservice
    executor.shutdown();
  }

  private static class MyRunnable implements Runnable {

    private final AtomicInteger invocationDone;

    public MyRunnable(AtomicInteger invocationDone) {
      this.invocationDone = invocationDone;
    }

    @Override
    public void run() {
      System.out.println(Thread.currentThread().getName() + ", execution");
      if (invocationDone.decrementAndGet() == 0) {
        throw new IllegalArgumentException("ohhh an Exception in MyRunnable");
      }
    }
  }
}

输出:

执行 get() 之前
池中的线程-1,正在执行
池中的线程-1,正在执行
池中的线程-1,正在执行
捕获到异常:java.lang.IllegalArgumentException:在 MyRunnable 中发生异常

示例:当任务执行遇到问题时,可继续进行调度

它执行一个任务,在前两次执行中抛出异常,第三次执行时抛出错误。可以看到,任务的客户端可以选择停止或不停止调度:在这里,如果遇到异常则继续进行,如果遇到错误则停止。

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ScheduledExecutorServiceWithException {

  public static void main(String[] args) {
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

    // variable used to thrown an error at the 3rd task invocation
    AtomicInteger countBeforeError = new AtomicInteger(3);

    // boolean allowing to leave the client to halt the scheduling task or not after a failure
    boolean mustHalt = true;
    do {
      Future<?> futureA = executor
              .scheduleWithFixedDelay(new MyRunnable(countBeforeError), 1, 2, TimeUnit.SECONDS);
      try {
        futureA.get(); // will return only if canceled
      } catch (InterruptedException e) {
        // handle that : halt or not halt
      } catch (ExecutionException e) {
        if (e.getCause() instanceof Error) {
          System.out.println("I halt in case of Error");
          mustHalt = true;
        } else {
          System.out.println("I reschedule in case of Exception");
          mustHalt = false;
        }
      }
    }
    while (!mustHalt);
    // shutdown the executorservice
    executor.shutdown();
  }

  private static class MyRunnable implements Runnable {

    private final AtomicInteger invocationDone;

    public MyRunnable(AtomicInteger invocationDone) {
      this.invocationDone = invocationDone;
    }

    @Override
    public void run() {
      System.out.println(Thread.currentThread().getName() + ", execution");

      if (invocationDone.decrementAndGet() == 0) {
        throw new Error("ohhh an Error in MyRunnable");
      } else {
        throw new IllegalArgumentException("ohhh an Exception in MyRunnable");
      }
    }
  }
}

输出:

池-1-线程-1,执行
出现异常时,我会重新安排任务
池-1-线程-1,执行
出现异常时,我会重新安排任务
池-1-线程-2,执行
出现错误时,我将停止运行

6

我知道这是一个老问题,但是如果有人正在使用延迟的CompletableFutureScheduledExecutorService,那么应该按照以下方式处理:

private static CompletableFuture<String> delayed(Duration delay) {
    CompletableFuture<String> delayed = new CompletableFuture<>();
    executor.schedule(() -> {
        String value = null;
        try {
            value = mayThrowExceptionOrValue();
        } catch (Throwable ex) {
            delayed.completeExceptionally(ex);
        }
        if (!delayed.isCompletedExceptionally()) {
            delayed.complete(value);
        }
    }, delay.toMillis(), TimeUnit.MILLISECONDS);
    return delayed;
}

处理CompletableFuture中的异常:

CompletableFuture<String> delayed = delayed(Duration.ofSeconds(5));
delayed.exceptionally(ex -> {
    //handle exception
    return null;
}).thenAccept(value -> {
    //handle value
});

6
另一个解决方案是在Runnable中捕获异常。例如,您可以使用jcabi-log中方便的VerboseRunnable类:
import com.jcabi.log.VerboseRunnable;
scheduler.scheduleWithFixedDelay(
  new VerboseRunnable(
    Runnable() {
      public void run() { 
        // do business logic, may Exception occurs
      }
    },
    true // it means that all exceptions will be swallowed and logged
  ),
  1, 10, TimeUnit.SECONDS
);

6
一种优雅的方法来捕获异常并保持计划任务的活动。
首先,定义一个函数接口。
    @FunctionalInterface
    interface NoSuppressedRunnable extends Runnable {

        @Override
        default void run() {
            try {
                doRun();
            } catch (Exception e) {
                log.error("...", e);
            }
        }


        void doRun();

    }

然后,像这样提交工作。

executorService.scheduleAtFixedRate((NoSuppressedRunnable) () -> {
    // Complier implies that this is an implement of doRun() once you put the cast above
}, 0, 60L, TimeUnit.SECONDS);

4
受@MBec解决方案的启发,我编写了一个很好的通用包装器,适用于ScheduledExecutorService,它具有以下功能:
  • 捕获并打印任何未处理的抛出异常。
  • 返回Java 8 CompletableFuture而不是Future。
:)
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * This class use as a wrapper for the Native Java ScheduledExecutorService class.
 * It was created in order to address the very unpleasant scenario of silent death!
 * explanation: each time an unhandled exception get thrown from a running task that runs by ScheduledExecutorService
 * the thread will die and the exception will die with it (nothing will propagate back to the main thread).
 *
 * However, HonestScheduledExecutorService will gracefully print the thrown exception with a custom/default message,
 * and will also return a Java 8 compliant CompletableFuture for your convenience :)
 */
@Slf4j
public class HonestScheduledExecutorService {

    private final ScheduledExecutorService scheduledExecutorService;
    private static final String DEFAULT_FAILURE_MSG = "Failure occurred when running scheduled task.";

    HonestScheduledExecutorService(ScheduledExecutorService scheduledExecutorService) {
        this.scheduledExecutorService = scheduledExecutorService;
    }

    public CompletableFuture<Object> scheduleWithFixedDelay(Callable callable, String onFailureMsg, long initialDelay, long delay, TimeUnit unit) {
        final String msg = StringUtils.isEmpty(onFailureMsg) ? DEFAULT_FAILURE_MSG : onFailureMsg;
        CompletableFuture<Object> delayed = new CompletableFuture<>();

        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            try {
                Object result = callable.call();
                delayed.complete(result);
            } catch (Throwable th) {
                log.error(msg, th);
                delayed.completeExceptionally(th);
            }
        }, initialDelay, delay, unit);

        return delayed;
    }

    public CompletableFuture<Void> scheduleWithFixedDelay(Runnable runnable, String onFailureMsg, long initialDelay, long delay, TimeUnit unit) {
        final String msg = StringUtils.isEmpty(onFailureMsg) ? DEFAULT_FAILURE_MSG : onFailureMsg;
        CompletableFuture<Void> delayed = new CompletableFuture<>();

        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            try {
                runnable.run();
                delayed.complete(null);
            } catch (Throwable th) {
                log.error(msg, th);
                delayed.completeExceptionally(th);
            }
        }, initialDelay, delay, unit);

        return delayed;
    }

    public CompletableFuture<Object> schedule(Callable callable, String failureMsg, long delay, TimeUnit unit) {
        final String msg = StringUtils.isEmpty(failureMsg) ? DEFAULT_FAILURE_MSG : failureMsg;
        CompletableFuture<Object> delayed = new CompletableFuture<>();

        scheduledExecutorService.schedule(() -> {
            try {
                Object result = callable.call();
                delayed.complete(result);
            } catch (Throwable th) {
                log.error(msg, th);
                delayed.completeExceptionally(th);
            }
        }, delay, unit);

        return delayed;
    }

    public CompletableFuture<Void> schedule(Runnable runnable, String failureMsg, long delay, TimeUnit unit) {
        final String msg = StringUtils.isEmpty(failureMsg) ? DEFAULT_FAILURE_MSG : failureMsg;
        CompletableFuture<Void> delayed = new CompletableFuture<>();

        scheduledExecutorService.schedule(() -> {
            try {
                runnable.run();
                delayed.complete(null);
            } catch (Throwable th) {
                log.error(msg, th);
                delayed.completeExceptionally(th);
            }
        }, delay, unit);

        return delayed;
    }

    public CompletableFuture<Object> scheduleAtFixedRate(Callable callable, String failureMsg, long initialDelay, long period, TimeUnit unit) {
        final String msg = StringUtils.isEmpty(failureMsg) ? DEFAULT_FAILURE_MSG : failureMsg;
        CompletableFuture<Object> delayed = new CompletableFuture<>();

        scheduledExecutorService.scheduleAtFixedRate(() -> {
            try {
                Object result = callable.call();
                delayed.complete(result);
            } catch (Throwable th) {
                log.error(msg, th);
                delayed.completeExceptionally(th);
            }
        }, initialDelay, period, unit);

        return delayed;
    }

    public CompletableFuture<Void> scheduleAtFixedRate(Runnable runnable, String failureMsg, long initialDelay, long period, TimeUnit unit) {
        final String msg = StringUtils.isEmpty(failureMsg) ? DEFAULT_FAILURE_MSG : failureMsg;
        CompletableFuture<Void> delayed = new CompletableFuture<>();

        scheduledExecutorService.scheduleAtFixedRate(() -> {
            try {
                runnable.run();
                delayed.complete(null);
            } catch (Throwable th) {
                log.error(msg, th);
                delayed.completeExceptionally(th);
            }
        }, initialDelay, period, unit);

        return delayed;
    }

    public CompletableFuture<Object> execute(Callable callable, String failureMsg) {
        final String msg = StringUtils.isEmpty(failureMsg) ? DEFAULT_FAILURE_MSG : failureMsg;
        CompletableFuture<Object> delayed = new CompletableFuture<>();

        scheduledExecutorService.execute(() -> {
            try {
                Object result = callable.call();
                delayed.complete(result);
            } catch (Throwable th) {
                log.error(msg, th);
                delayed.completeExceptionally(th);
            }
        });

        return delayed;
    }

    public CompletableFuture<Void> execute(Runnable runnable, String failureMsg) {
        final String msg = StringUtils.isEmpty(failureMsg) ? DEFAULT_FAILURE_MSG : failureMsg;
        CompletableFuture<Void> delayed = new CompletableFuture<>();

        scheduledExecutorService.execute(() -> {
            try {
                runnable.run();
                delayed.complete(null);
            } catch (Throwable th) {
                log.error(msg, th);
                delayed.completeExceptionally(th);
            }
        });

        return delayed;
    }

    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
        return scheduledExecutorService.awaitTermination(timeout, unit);
    }

    public List<Runnable> shutdownNow() {
        return scheduledExecutorService.shutdownNow();
    }

    public void shutdown() {
        scheduledExecutorService.shutdown();
    }

}

虽然我不确定为执行器服务创建这种一刀切的处理程序是否有实际意义,但如果您要这样做,为什么不声明您实现了 ScheduledExecutorService 接口,并使用 @Override 标记您的方法以明确您的意图并让编译器检查您的工作? - Basil Bourque
因为新的方法没有相同的签名。 - dorony

2

当线程的run()方法中抛出异常并传递给ScheduledExecutorService时,异常不会被抛出。如果我们使用future.get()来获取状态,则主线程会无限期地等待。


2
为了避免阻塞,您可以使用 isDone() 来检查 ScheduledFuture 是否已完成,如果是,则使用 get() 使其抛出 ExecutionException,然后您可以处理它。或者只需使用超时参数设置为零的 get 方法。 - Martin
@Martin,你能详细解释一下吗?我的应用在后台运行时,在特定条件下会将设备静音。它的运行非常完美,但是在一段时间后,应用程序突然崩溃了,我除了强制停止应用并重新启动之外无能为力。 - Teekam Suthar

1

就我个人而言,我不同意这里的所有答案。它们的主要问题是都提供了相同的解决方案,只是呈现方式不同。相反,你应该创建自己的线程工厂,在创建的线程上安装未捕获异常处理程序。例如,这是任何会自行创建线程的执行器中安装的 DefaultThreadFactory。遗憾的是,截至Java 11,它仍然是一个私有类,因为我想扩展它而不是将其复制到我的代码库中。以下是在Executors.java文件中出现的代码片段。

    private static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

正如您所见,接口本身是一个处理创建新线程的单个方法。除了确定创建线程工厂的线程组之外,它并没有太多的魔力。有趣的部分是线程被创建为非守护进程。

当线程被创建时,您可以调用setThreadUncaughtExceptionHandler,它接受一个处理程序,在其中您应该处理在该线程中发生的任何未捕获异常。默认情况下,它将从您的线程组继承,该线程组具有以下特性:

    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

默认情况下,如果存在父线程组,则尝试委托处理,并仅在测试平台默认未捕获的异常处理程序时执行。通常不会明确安装它。如果您想对不知道此事的可怜代码库造成一些真正的损害,可以通过Thread#setDefaultUncaughtExceptionHandler安装一个异常处理程序。别担心,如果运行时有安全管理器,您将无法这样做。

如果您要安装自己的处理程序,则该处理程序将被调用而不是组处理程序。

现在,回答您的问题:如何在执行程序中处理异常。默认情况下,如果代码无法处理自己的错误,则认为线程已经死亡。我认为您应该遵循这个规则。未捕获的异常处理程序无法挽救您的线程。相反,它将帮助您诊断发生了什么。进入ScheduledExecutor实现,它允许定期执行可运行项,同样的规则适用:如果一个执行失败,线程将被终止,以及应该运行的可运行项。

简而言之,处理自己的错误。我们有检查异常是有原因的。

但是未检查的异常呢?

有趣的是,我将犯与其他帖子相同的错误:在Throwable上使用try/catch,但断言它不是ThreadDeath错误。如果你确实得到了一个ThreadDeath错误,你必须重新抛出它以确保线程真正死亡。

1
本帖主要讨论ScheduledExecutor。UCH不适用于此。大部分内容都是关于解决非ScheduledExecutor情况下的问题。 - Jeffrey Blattman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接