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


73

在某些代码块的运行时间超过可接受的时间之后,是否可以强制Java引发Exception?

Answers:


23

是的,但是强制另一个线程在随机的代码行上中断通常是一个非常糟糕的主意。仅当您打算关闭该过程时才这样做。

您可以做的是Thread.interrupt()在一定时间后用于任务。但是,除非代码对此进行检查,否则它将无法正常工作。ExecutorService可以使此操作更容易Future.cancel(true)

代码自动计时并在需要时停止会更好。


1
问题是我有一个第三方库,有时它运行时间太长,并且没有超时的本机机制
htf 2011年

2
不幸的是,这是一个常见的问题,解决此问题的唯一可靠方法是拥有一个单独的过程,您可以将其杀死。另一种方法是在其上使用Thread.stop()。使用该方法之前,请阅读此方法的警告!
彼得·劳瑞

53

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

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的性能在此处(stackoverflow.com/a/27025552/2116890)受到了良好的评价,在这里被认为是很棒的。在那种情况下,他们要处理更多线程。以我的经验,无论使用单个线程还是使用多个线程,我都没有注意到开销,但是我也没有以任何方式相对于替代方案来衡量其性能。
user2116890

1
之后executor.shutdownNow(),您可能想while (true) {try {if (executor.awaitTermination(1, TimeUnit.SECONDS)) break;} catch (InterruptedException ie) {}}要这样做,因为实际停止任务可能需要一些时间。
安德斯·卡塞格

这里要注意的有趣事情。超时并不表示基础任务已停止。取消了将来,并且executorService对此有所了解,但是线程可能仍在继续工作而无需关心终止,这意味着它可能仍然浪费了您希望释放的资源。
Tarmo

42

我将其他一些答案编译为一个实用程序方法:

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!

能否请您添加一些使用示例?尽管进行了设置,但runWithTimeout即使我将超时设置为150分钟,它也会在执行该方法后立即引发异常。
StepTNT 2014年

2
现在,我添加了示例代码并改进了原始代码,以在超时的情况下取消已提交的任务。我将不胜感激;-)
Neeme Praks

@htf,这确实应该接受的答案..干得漂亮
卡洛斯-

1
@ AliReza19330,是的,我不好。你是对的。从性能角度来看,为每个用户启动一个新线程可能不是最好的主意。:-P
Neeme Praks

1
@NeemePraks我认为“ throw(Exception)e;” 应为“ throw(Exception)t;” 在第二个runWithTimeout方法中
Alb

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 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();
}

3

我可以提出两个选择。

  1. 在该方法中,假设它正在循环并且不等待外部事件,则添加一个本地字段并在每次循环时测试时间。

    void method() {
        long endTimeMillis = System.currentTimeMillis() + 10000;
        while (true) {
            // method logic
            if (System.currentTimeMillis() > endTimeMillis) {
                // do some clean-up
                return;
            }
        }
    }
    
  2. 在线程中运行该方法,并使调用者计数为10秒。

    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
+0:在您的第二个示例中,您是否尝试Thread.join(long)使用超时;)
Peter Lawrey

2
之后,thread.start()您可以拥有thread.join(10000);其余的代码。
彼得·劳里

3

编辑:彼得·劳里(Peter Lawrey)完全正确:它不像中断线程那样简单(我的最初建议),执行器和可调用对象非常有用...

无需中断线程,而是可以在达到超时后在Callable上设置一个变量。可调用对象应在任务执行的适当位置检查此变量,以了解何时停止。

可赎回债券返回期货,您可以在尝试“获取”期货结果时指定超时。像这样:

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

请参见Future.get,执行程序和可调用...

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
不幸的是,只有在您控制可调用对象中执行的代码时,该方法才有效(如果为true,那么这样做很简单)
Chii

好的,我现在可以在@HTF的评论中看到这一点。我想Thread.stop()是唯一的方法!在此处查看警告:download.oracle.com/javase/6/docs/technotes/guides/concurrency/…
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。我使用了琐碎的逻辑,即通过另一个线程执行它,并通过指定超时来等待其将来的对象。如果超时,这将给我一个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,并且在推送消息时,我的代码逻辑遇到了指定的超时。现在,实际上我的消息已被推送到队列中,但由于TIMEOUT异常,我的主线程认为该消息失败。这是可以避免而不是解决的问题。就像我的情况一样,我通过提供一个在几乎所有情况下都足够的超时来避免这种情况。

如果您要中断的代码在应用程序内部,并且不像API调用,那么您可以简单地使用

future.cancel(true)

但是请记住,java docs说它确实保证了执行将被阻止。

“尝试取消执行此任务。如果任务已经完成,已经被取消或由于其他原因无法取消,则此尝试将失败。如果成功,并且在调用cancel时此任务尚未开始,则此操作将失败。任务永远不会运行。如果任务已经启动,则mayInterruptIfRunning参数确定执行该任务的线程是否应在尝试停止该任务时被中断。


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以便在引发异常时重试该方法三次。


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
你变TimeOut timeOutmain()方法,从未使用过。
约翰内斯

2
在主要方法中,应该使用Thread timeOutThread = new Thread(timeOut);@Johannes timeOut。
Arjun Varshney

请注意,这是唯一可扩展的解决方案。您实际上只需要一个计时器线程,这可能会中断数千个工作线程。相反,您有成千上万个用于数千个工作线程的计时器线程-至少可以说这不是理想的选择。
阿古斯顿·霍瓦斯

0

有一种方法可以做到。

设置一些布尔值字段以指示工作是否完成。然后在代码块之前,设置一个计时器,以在超时后运行一段代码。计时器将检查代码块是否已完成执行,否则,将引发异常。否则它将无能为力。

当然,代码块的末尾应将字段设置为true,以指示工作已完成。


这不是100%正确的,因为计时器线程将独立于运行代码块的线程。尽管“工作者”线程没有执行任何操作,但计时器线程可能会引发异常,因为还有其他线程具有更高的优先级。“运行时间超过X秒”和“已在X秒之前开始”之间有细微的差别。
2011年

好吧,很公平 感谢您的回复。我将保留答案,以便其他人可以看到您的评论
HXCaine 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.