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

是否有可能强制 Java 在某些代码块运行时间超过可接受时抛出 Exception?

153331 次浏览

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

您可以做的是在一定的时间后对任务使用 Thread.interrupt()。然而,除非代码对此进行检查,否则它不会工作。使用 Future.cancel(true),ExecutorService 可以简化这一过程

对于代码来说,计时自身并在需要时停止要好得多。

有一种粗糙的方法可以做到这一点。

设置一些布尔字段以指示工作是否已完成。然后在代码块之前,设置一个计时器在超时后运行一段代码。计时器将检查代码块是否已经完成执行,如果没有,则抛出异常。否则它什么也做不了。

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

编辑: Peter Lawrey 是完全正确的: 它不像打断一个线程那么简单(我最初的建议) ,而且执行者和可调用程序是非常有用的..。

一旦超时,您可以在 Callable 上设置一个变量,而不是中断线程。可调用方应该在任务执行的适当位置检查该变量,以知道何时停止。

可调用类返回 Futures,当您尝试“获取”未来的结果时,可以用它指定一个超时。就像这样:

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

查看未来。获取,执行器,和可呼叫..。

Https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/future.html#get-long-java.util.concurrent 时间单元

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. 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) {}
    }
    

The drawback to this approach is that method() cannot return a value directly, it must update an instance field to return its value.

如果需要计时的是测试代码,那么可以使用 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();
}

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

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

I compiled some of the other answers into a single utility method:

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!

不要在新线程中设置任务,在主线程中设置计时器,而是在新线程中设置计时器,在主线程中设置任务:

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 }
}

我创建了一个非常简单的解决方案,没有使用任何框架或 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 连接挂起或完全中断。这导致整个系统瘫痪。我需要一种方法来检测它,并阻止它的发生。所以我创造了这个并使用它。效果很好。

如果需要 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);
}
}

If you're using spring, you could annotate the method with a @Retryable so that it retries the method three times if an exception is thrown.

我遇到了类似的问题,我的任务是在特定的超时时间内将消息推送到 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,当消息被推送时,我的代码逻辑遇到了指定的超时。现在,实际上我的消息被推送到 Queue 中,但是我的主线程认为它由于 TIMEOUT 异常而失败。 This is a type of problem which can be avoided rather than being solved. Like in my case I avoided it by providing a timeout which would suffice in nearly all of the cases.

如果您想要中断的代码位于您的应用程序中,而不是类似于 API 调用的代码,那么您可以简单地使用

future.cancel(true)

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

”试图取消此任务的执行。如果任务已经完成、已经取消或由于其他原因无法取消,则此尝试将失败。如果成功,并且在调用取消时此任务尚未启动,则此任务永远不应运行。如果任务已经启动,那么 mayInterrupIfRun 参数将确定执行此任务的线程是否应该中断以试图停止该任务。”

有一个很简单的选择,还没有人提到过:

Duration timeout = Duration.ofMinutes(5);
Thread thread = new Thread(() -> {
// your code here
});
thread.start();
thread.join(timeout.toMillis());
if (thread.isAlive()) {
thread.interrupt();
throw new MyTimeoutException();
}

如果运行代码块的线程未能在超时时间内完成,则会中断该线程,并且可以抛出所需的任何异常。

有可能编写的代码只是忽略中断并继续执行。如果你处理这个无法修复它,那么有 thread.stop(),但这可以打破任何同步机制,你是依赖。看它的 deprecation notice

还可以从线程捕获异常:

AtomicReference<Throwable> uncaughtException = new AtomicReference<>();
thread.setUncaughtExceptionHandler((t, ex) -> uncaughtException.setRelease(ex));


// ...


Throwable ex = uncaughtException.getAcquire();
if (ex != null) {
throw ex;
}

我也有这个问题,我的日志打印出“’意外的结束流’’。无法从池中获取资源, 我将 brpop 的超时设置为30秒,将 redis 设置为31秒,将 mysql 数据库连接池设置为300秒。目前,这个错误还没有打印到日志中,但是我不知道这个错误是否会在将来被报告。我不知道它是否会对我写入数据库的过程产生不好的影响