Java:如何在特定代码块上设置超时时间?

82

有没有办法在一段代码块执行的时间超出可接受范围后强制Java抛出异常?

13个回答

56
这是我所知道的最简单的方法:

以下是具体步骤:

final Runnable stuffToDo = new Thread() {
  @Override 
  public void run() { 
    /* Do stuff here. */ 
  }
};

final ExecutorService executor = Executors.newSingleThreadExecutor();
final Future future = executor.submit(stuffToDo);
executor.shutdown(); // This does not cancel the already-scheduled task.

try { 
  future.get(5, TimeUnit.MINUTES); 
}
catch (InterruptedException ie) { 
  /* Handle the interruption. Or ignore it. */ 
}
catch (ExecutionException ee) { 
  /* Handle the error. Or ignore it. */ 
}
catch (TimeoutException te) { 
  /* Handle the timeout. Or ignore it. */ 
}
if (!executor.isTerminated())
    executor.shutdownNow(); // If you want to stop the code that hasn't finished.

另外,您可以创建一个TimeLimitedCodeBlock类来包装此功能,然后您可以在需要时使用它,如下所示:

new TimeLimitedCodeBlock(5, TimeUnit.MINUTES) { @Override public void codeBlock() {
    // Do stuff here.
}}.run();

刚看到这个。做类似这样的事情的开销是多少?我觉得如果你经常在stuffToDo中做一些事情,每次创建一个新的单线程执行器都是昂贵的,但我不确定,因此提出了我的问题。 - MarkII
我从未遇到过这种方法的性能问题。在某些情况下,如果您认为可以专门针对仅具有一个线程的情况进行更轻量级的版本设计,那么创建自己的_Executor_实现可能更可取。 - user2116890
ExecutorService 的性能在这里(https://dev59.com/DnI-5IYBdhLWcg3w1sXi#27025552)得到了良好的评估,被认为是非常棒的。在那种情况下,他们处理了更多的线程。根据我的经验,无论是使用单个线程还是多个线程,我从未注意到开销,但我也没有努力以任何方式测量其相对于替代方案的性能。 - user2116890
1
在执行 executor.shutdownNow() 后,你可能想要使用 while (true) {try {if (executor.awaitTermination(1, TimeUnit.SECONDS)) break;} catch (InterruptedException ie) {}},因为实际停止任务可能需要一些时间。 - Anders Kaseorg
这里需要注意的有趣的事情是,超时并不意味着底层任务已经停止。未来被取消并且executorService也被通知了,但线程可能仍在继续工作而不关心终止,这意味着它可能仍然浪费你希望释放的资源。 - Tarmo
@Tarmo executor.shutdownNow()最终停止线程。另外,在上面的示例中,您需要恢复中断标志。 - undefined

47

我将其他答案编译成一个单一的实用方法:

public class TimeLimitedCodeBlock {

  public static void runWithTimeout(final Runnable runnable, long timeout, TimeUnit timeUnit) throws Exception {
    runWithTimeout(new Callable<Object>() {
      @Override
      public Object call() throws Exception {
        runnable.run();
        return null;
      }
    }, timeout, timeUnit);
  }

  public static <T> T runWithTimeout(Callable<T> callable, long timeout, TimeUnit timeUnit) throws Exception {
    final ExecutorService executor = Executors.newSingleThreadExecutor();
    final Future<T> future = executor.submit(callable);
    executor.shutdown(); // This does not cancel the already-scheduled task.
    try {
      return future.get(timeout, timeUnit);
    }
    catch (TimeoutException e) {
      //remove this if you do not want to cancel the job in progress
      //or set the argument to 'false' if you do not want to interrupt the thread
      future.cancel(true);
      throw e;
    }
    catch (ExecutionException e) {
      //unwrap the root cause
      Throwable t = e.getCause();
      if (t instanceof Error) {
        throw (Error) t;
      } else if (t instanceof Exception) {
        throw (Exception) t;
      } else {
        throw new IllegalStateException(t);
      }
    }
  }

}

使用此实用程序方法的示例代码:

示例代码:

  public static void main(String[] args) throws Exception {
    final long startTime = System.currentTimeMillis();
    log(startTime, "calling runWithTimeout!");
    try {
      TimeLimitedCodeBlock.runWithTimeout(new Runnable() {
        @Override
        public void run() {
          try {
            log(startTime, "starting sleep!");
            Thread.sleep(10000);
            log(startTime, "woke up!");
          }
          catch (InterruptedException e) {
            log(startTime, "was interrupted!");
          }
        }
      }, 5, TimeUnit.SECONDS);
    }
    catch (TimeoutException e) {
      log(startTime, "got timeout!");
    }
    log(startTime, "end of main method!");
  }

  private static void log(long startTime, String msg) {
    long elapsedSeconds = (System.currentTimeMillis() - startTime);
    System.out.format("%1$5sms [%2$16s] %3$s\n", elapsedSeconds, Thread.currentThread().getName(), msg);
  }

在我的计算机上运行示例代码的输出:

    0ms [            main] calling runWithTimeout!
   13ms [ pool-1-thread-1] starting sleep!
 5015ms [            main] got timeout!
 5016ms [            main] end of main method!
 5015ms [ pool-1-thread-1] was interrupted!

2
我现在添加了示例代码并改进了原始代码,以便在超时的情况下取消已提交的任务。如果您能点个赞,我将不胜感激;-) - Neeme Praks
@htf,这真的应该成为被接受的答案。 做得好。 - Carlos
@NeemePraks 谢谢你,但是每个调用 Web 服务的用户实际上都会启动一个新线程,然后每个请求都有一个 ExecutorService 线程,实际上并不是所有用户都会等待单个线程。我说得对吗? - Ali Abazari
1
@AliReza19330,是的,我的错。你是对的。为每个用户启动一个新线程可能不是性能上最好的想法。:-P - Neeme Praks
1
@NeemePraks 我认为第二个 runWithTimeout 方法中的 "throw (Exception) e;" 应该改为 "throw (Exception) t;"。 - Alb
显示剩余5条评论

26

是的,但通常强制另一个线程在代码的任意行中断是一个非常糟糕的主意。只有在想要关闭进程时才会这样做。

你可以在一定时间后使用Thread.interrupt()来终止一个任务。但是,除非代码对此进行检查,否则它将不起作用。使用Future.cancel(true)可以让ExecutorService更容易实现此目的。

最好的做法是让代码自行计时并在需要时停止。


1
问题在于我有一个第三方库,有时运行时间太长,而且没有本地机制来设置超时。 - htf
2
这是一个常见的问题,不幸的是唯一可靠的方法是拥有一个可以终止的单独进程。另一种选择是使用Thread.stop()。在使用之前请阅读该方法的警告! - Peter Lawrey

10

如果你想测试代码的运行时间,那么你可以使用time属性:

@Test(timeout = 1000)  
public void shouldTakeASecondOrLess()
{
}

如果这是生产代码,那么就没有简单的机制,你可以使用哪种解决方案取决于你是否可以改变代码以进行计时。

如果您可以更改被计时的代码,则一种简单的方法是让您的计时代码记住其开始时间,并定期将当前时间与该时间进行比较。例如:

long startTime = System.currentTimeMillis();
// .. do stuff ..
long elapsed = System.currentTimeMillis()-startTime;
if (elapsed>timeout)
   throw new RuntimeException("tiomeout");

如果代码本身无法检查超时,您可以在另一个线程上执行代码,并等待完成或超时。

    Callable<ResultType> run = new Callable<ResultType>()
    {
        @Override
        public ResultType call() throws Exception
        {
            // your code to be timed
        }
    };

    RunnableFuture<ResultType> future = new FutureTask<>(run);
    ExecutorService service = Executors.newSingleThreadExecutor();
    service.execute(future);
    ResultType result = null;
    try
    {
        result = future.get(1, TimeUnit.SECONDS);    // wait 1 second
    }
    catch (TimeoutException ex)
    {
        // timed out. Try to stop the code if possible.
        future.cancel(true);
    }
    service.shutdown();
}

4
我可以提供两个选项。
  1. Within the method, assuming it is looping and not waiting for an external event, add a local field and test the time each time around the loop.

    void method() {
        long endTimeMillis = System.currentTimeMillis() + 10000;
        while (true) {
            // method logic
            if (System.currentTimeMillis() > endTimeMillis) {
                // do some clean-up
                return;
            }
        }
    }
    
  2. Run the method in a thread, and have the caller count to 10 seconds.

    Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                method();
            }
    });
    thread.start();
    long endTimeMillis = System.currentTimeMillis() + 10000;
    while (thread.isAlive()) {
        if (System.currentTimeMillis() > endTimeMillis) {
            // set an error flag
            break;
        }
        try {
            Thread.sleep(500);
        }
        catch (InterruptedException t) {}
    }
    
这种方法的缺点是method()无法直接返回值,必须更新实例字段以返回其值。

1
在你的第二个示例中,你是不是试图使用Thread.join(long)并设置超时时间呢? ;) - Peter Lawrey
3
thread.start() 之后,你可以使用 thread.join(10000) 替换掉剩余的代码。 - Peter Lawrey

3

编辑:Peter Lawrey是完全正确的:中断线程并不像我最初的建议那样简单,而且Executors和Callables非常有用...

与其中断线程,你可以在Callable上设置一个变量,一旦达到超时时间就会触发。Callable应该在任务执行的适当时点检查此变量,以知道何时停止。

Callables返回Futures,在尝试“获取”未来结果时,您可以指定超时时间。类似于这样:

try {
   future.get(timeoutSeconds, TimeUnit.SECONDS)
} catch(InterruptedException e) {
   myCallable.setStopMeAtAppropriatePlace(true);
}

请参见 Future.get、Executors 和 Callable...

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html#get-long-java.util.concurrent.TimeUnit-

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Callable.html

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#newFixedThreadPool%28int%29


1
不幸的是,只有在您控制可调用函数内执行的代码时才有效(如果是这样,那么这样做就很简单)。 - Chii
好的,我现在可以看到@HTF评论中的内容了。我想Thread.stop()是唯一的方法!请参见此处的警告:http://download.oracle.com/javase/6/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html - laher

2

我创建了一个非常简单的解决方案,没有使用任何框架或API。这看起来更优雅和易懂。该类被称为TimeoutBlock。

public class TimeoutBlock {

 private final long timeoutMilliSeconds;
    private long timeoutInteval=100;

    public TimeoutBlock(long timeoutMilliSeconds){
        this.timeoutMilliSeconds=timeoutMilliSeconds;
    }

    public void addBlock(Runnable runnable) throws Throwable{
        long collectIntervals=0;
        Thread timeoutWorker=new Thread(runnable);
        timeoutWorker.start();
        do{ 
            if(collectIntervals>=this.timeoutMilliSeconds){
                timeoutWorker.stop();
                throw new Exception("<<<<<<<<<<****>>>>>>>>>>> Timeout Block Execution Time Exceeded In "+timeoutMilliSeconds+" Milli Seconds. Thread Block Terminated.");
            }
            collectIntervals+=timeoutInteval;           
            Thread.sleep(timeoutInteval);

        }while(timeoutWorker.isAlive());
        System.out.println("<<<<<<<<<<####>>>>>>>>>>> Timeout Block Executed Within "+collectIntervals+" Milli Seconds.");
    }

    /**
     * @return the timeoutInteval
     */
    public long getTimeoutInteval() {
        return timeoutInteval;
    }

    /**
     * @param timeoutInteval the timeoutInteval to set
     */
    public void setTimeoutInteval(long timeoutInteval) {
        this.timeoutInteval = timeoutInteval;
    }
}

例子:

try {
        TimeoutBlock timeoutBlock = new TimeoutBlock(10 * 60 * 1000);//set timeout in milliseconds
        Runnable block=new Runnable() {

            @Override
            public void run() {
                //TO DO write block of code to execute
            }
        };

        timeoutBlock.addBlock(block);// execute the runnable block 

    } catch (Throwable e) {
        //catch the exception here . Which is block didn't execute within the time limit
    }

当我需要连接FTP帐户并下载和上传文件时,这对我非常有用。有时候FTP连接会卡住或完全中断,导致整个系统崩溃。我需要一种检测它并防止发生的方法。所以我创建了这个并使用了它。效果相当不错。


2

我曾经遇到类似的问题,我的任务是在特定的超时时间内将消息推送到SQS。我使用了简单的逻辑,通过另一个线程执行并等待其future对象,并指定超时时间。如果超时,这将给我一个TIMEOUT异常。


final Future<ISendMessageResult> future = 
timeoutHelperThreadPool.getExecutor().submit(() -> {
  return getQueueStore().sendMessage(request).get();
});
try {
  sendMessageResult = future.get(200, TimeUnit.MILLISECONDS);
  logger.info("SQS_PUSH_SUCCESSFUL");
  return true;

} catch (final TimeoutException e) {
  logger.error("SQS_PUSH_TIMEOUT_EXCEPTION");
}

但是有些情况下,你无法阻止另一个线程执行代码,因此在这种情况下会得到真实的负面结果。
例如,在我的情况下,我的请求已经到达SQS,当消息正在被推送时,我的代码逻辑遇到了指定的超时。现实情况是我的消息已经被推送到队列中,但是我的主线程却认为它失败了,因为出现了超时异常。这是一种可以避免而不是解决的问题。就像我这种情况,我通过提供一个几乎在所有情况下都足够的超时时间来避免这个问题。
如果你想要中断的代码在你的应用程序内部,并且不是像API调用那样的东西,那么你可以简单地使用。原始答案:

future.cancel(true)

最初的回答:
请注意,Java文档说它不能保证任务一定会被阻塞。
“尝试取消此任务的执行。如果任务已经完成、已被取消或由于其他原因而无法取消,则此尝试将失败。如果成功,并且在调用取消时此任务尚未启动,则此任务不应运行。如果任务已经启动,则mayInterruptIfRunning参数确定是否应中断执行此任务的线程以尝试停止该任务。”

1

将计时器放在新线程中,将任务放在主线程中,而不是将任务放在新线程中,将计时器放在主线程中:

public static class TimeOut implements Runnable{
    public void run() {
        Thread.sleep(10000);
        if(taskComplete ==false) {
            System.out.println("Timed Out");
            return;
        }
        else {
            return;
        }
    }
}
public static boolean taskComplete = false;
public static void main(String[] args) {
    TimeOut timeOut = new TimeOut();
    Thread timeOutThread = new Thread(timeOut);
    timeOutThread.start();
    //task starts here
    //task completed
    taskComplete =true;
    while(true) {//do all other stuff }
}

1
你在 main() 方法中定义的变量 TimeOut timeOut 从未被使用。 - Johannes
2
在主方法中应该是 Thread timeOutThread = new Thread(timeOut); 然后使用 @Johannes 的 timeOut。 - Arjun Varshney
请注意,这是唯一可扩展的解决方案。您实际上只需要一个计时器线程,它可以中断数千个工作线程。反之,如果有数千个计时器线程用于数千个工作线程,则不理想至少可以说。 - Agoston Horvath

1
如果您想使用CompletableFuture方式,可以编写一个类似以下方法的函数:
public MyResponseObject retrieveDataFromEndpoint() {

   CompletableFuture<MyResponseObject> endpointCall 
       = CompletableFuture.supplyAsync(() ->
             yourRestService.callEnpoint(withArg1, withArg2));

   try {
       return endpointCall.get(10, TimeUnit.MINUTES);
   } catch (TimeoutException 
               | InterruptedException 
               | ExecutionException e) {
       throw new RuntimeException("Unable to fetch data", e);
   }
}

如果您正在使用Spring,您可以使用@Retryable注释该方法,以便在抛出异常时重试该方法三次。

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