应永久运行的任务的 Java 执行器最佳实践

2022-09-01 06:10:51

我正在处理一个Java项目,我需要异步运行多个任务。我被引导相信执行器是我做到这一点的最佳方式,所以我正在熟悉它。(耶伊获得报酬来学习!但是,我不清楚完成我想要做的事情的最佳方法是什么。

为了便于论证,假设我有两个任务在运行。两者都不应终止,并且两者都应在应用程序的生存期内运行。我正在尝试编写一个主包装类,以便:

  • 如果任一任务引发异常,包装器将捕获该任务并重新启动该任务。
  • 如果任一任务运行到完成,包装器将注意到并重新启动该任务。

现在,应该注意的是,这两个任务的实现都将代码包装在一个永远不会运行到完成的无限循环中,并具有一个 try/catch 块,该块应处理所有运行时异常而不会中断循环。我试图增加另一层确定性;如果我或跟随我的人做了一些愚蠢的事情,击败了这些安全措施并停止了任务,应用程序需要做出适当的反应。run()

有没有比我更有经验的人推荐的解决这个问题的最佳实践?

FWIW,我已经鞭打了这个测试类:


public class ExecTest {

   private static ExecutorService executor = null;
   private static Future results1 = null;
   private static Future results2 = null;

   public static void main(String[] args) {
      executor = Executors.newFixedThreadPool(2);
      while(true) {
         try {
            checkTasks();
            Thread.sleep(1000);
         }
         catch (Exception e) {
            System.err.println("Caught exception: " + e.getMessage());
         }
      }
   }

   private static void checkTasks() throws Exception{
      if (results1 == null || results1.isDone() || results1.isCancelled()) {
         results1 = executor.submit(new Test1());
      }

      if (results2 == null || results2.isDone() || results2.isCancelled()) {
         results2 = executor.submit(new Test2());
      }
   }
}

class Test1 implements Runnable {
   public void run() {
      while(true) {
         System.out.println("I'm test class 1");
         try {Thread.sleep(1000);} catch (Exception e) {}
      }

   }
}

class Test2 implements Runnable {
   public void run() {
      while(true) {
         System.out.println("I'm test class 2");
         try {Thread.sleep(1000);} catch (Exception e) {}
      }
   }
}

它的行为方式是我想要的,但我不知道是否有任何陷阱,低效率或彻头彻尾的错误等待着我感到惊讶。(事实上,鉴于我是新手,如果没有错误/不明智的事情,我会感到震惊。

欢迎任何见解。


答案 1

在我之前的项目中,我遇到了类似的情况,在我的代码在面对愤怒的客户时爆炸后,我和我的朋友增加了两个大的保安:

  1. 在无限循环中,也要捕获错误,而不仅仅是异常。有时,不明显的事情会发生,Java会向你抛出一个错误,而不是一个异常。
  2. 使用回退开关,这样如果出现问题且不可恢复,您就不会通过急切地启动另一个循环来升级情况。相反,您需要等到情况恢复正常,然后重新开始。

例如,我们遇到过数据库关闭的情况,在循环期间抛出了 SQLException。不幸的结果是,代码再次通过循环,只是再次遇到相同的异常,依此类推。日志显示,我们在一秒钟内命中了相同的SQLException大约300次!...这种情况断断续续地发生了几次,偶尔的JVM暂停了5秒左右,在此期间应用程序没有响应,直到最终抛出错误并且线程死亡!

因此,我们实现了一个回退策略,大致如下面的代码所示,如果异常不可恢复(或者在几分钟内被排除在外),那么我们在恢复操作之前等待更长的时间。

class Test1 implements Runnable {
  public void run() {
    boolean backoff = false;
    while(true) {
      if (backoff) {
        Thread.sleep (TIME_FOR_LONGER_BREAK);
        backoff = false;
      }
      System.out.println("I'm test class 1");
      try {
        // do important stuff here, use database and other critical resources
      }
      catch (SqlException se) {
       // code to delay the next loop
       backoff = true;
      }
      catch (Exception e) {
      }
      catch (Throwable t) {
      }
    }
  }
}

如果你以这种方式实现你的任务,那么我认为使用checkTasks()方法拥有第三个“看门狗”线程是没有意义的。此外,出于我上面概述的相同原因,我会谨慎地再次与执行器一起启动任务。首先,您需要了解任务失败的原因,以及环境是否处于稳定状态,再次运行任务会很有用。


答案 2

除了关注它之外,我通常还会针对PMDFindBugs等静态分析工具运行Java代码,以寻找更深层次的问题。

特别是对于此代码,FindBugs 不喜欢结果 1 和结果 2 在惰性 init 中不易失性,并且 run() 方法可能会忽略 Exception,因为它们没有被显式处理。

总的来说,我对使用Thread.sleep进行并发测试,更喜欢计时器或终止状态/条件持谨慎态度。Callable 可能有助于在发生中断时返回某些内容,如果无法计算结果,则会引发异常。

有关一些最佳实践和更多值得思考的信息,请查看并发实践。


推荐