【发布时间】:2011-08-08 14:01:46
【问题描述】:
是否可以在某些代码块运行时间超过可接受的时间后强制 Java 抛出异常?
【问题讨论】:
是否可以在某些代码块运行时间超过可接受的时间后强制 Java 抛出异常?
【问题讨论】:
这是我所知道的最简单的方法:
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();
【讨论】:
executor.shutdownNow() 之后,您可能想要while (true) {try {if (executor.awaitTermination(1, TimeUnit.SECONDS)) break;} catch (InterruptedException ie) {}},因为实际停止任务可能需要一些时间。
我将其他一些答案编译成一个实用方法:
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 分钟也是如此。
是的,但是强制另一个线程在随机的代码行上中断通常是一个非常糟糕的主意。仅当您打算关闭该进程时,您才会这样做。
您可以做的是在一定时间后将Thread.interrupt() 用于某项任务。但是,除非代码对此进行检查,否则它将无法正常工作。 ExecutorService 可以通过Future.cancel(true) 使这更容易
让代码自己计时并在需要时停止会更好。
【讨论】:
如果是要计时的测试代码,可以使用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();
}
【讨论】:
我可以建议两个选项。
在方法内,假设它正在循环并且不等待外部事件,添加一个本地字段并测试每次循环的时间。
void method() {
long endTimeMillis = System.currentTimeMillis() + 10000;
while (true) {
// method logic
if (System.currentTimeMillis() > endTimeMillis) {
// do some clean-up
return;
}
}
}
在一个线程中运行该方法,并让调用者计数为 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() 不能直接返回值,它必须更新实例字段以返回其值。
【讨论】:
Thread.join(long) ;)
thread.start() 之后你可以使用thread.join(10000); 而不是其余的代码。
编辑:Peter Lawrey 是完全正确的:它不像中断线程那么简单(我最初的建议),Executors & Callables 非常有用......
一旦达到超时,您可以在 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/Callable.html
【讨论】:
我创建了一个非常简单的解决方案,没有使用任何框架或 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 连接挂起或完全中断。这导致整个系统崩溃。我需要一种方法来检测它并防止它发生。所以我创建了这个并使用了它。效果很好。
【讨论】:
我遇到了类似的问题,我的任务是在特定超时内将消息推送到 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 文档说它确实保证执行将被阻止。
"尝试取消此任务的执行。如果该任务已经完成、已被取消或由于其他原因无法取消,则此尝试将失败。如果成功,则此任务调用cancel时尚未启动,该任务不应该运行。如果任务已经启动,则mayInterruptIfRunning参数决定是否应该中断执行该任务的线程以试图停止该任务。"
【讨论】:
如果你想要一个 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 注释该方法,以便在抛出异常时重试该方法 3 次。
【讨论】:
不要让新线程中的任务和主线程中的计时器,而是新线程中的计时器和主线程中的任务:
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 }
}
【讨论】:
main() 方法中的变量TimeOut timeOut 永远不会被使用。
Thread timeOutThread = new Thread(timeOut); 然后使用 @Johannes timeOut。
有一种方法可以做到这一点。
设置一些布尔字段来指示工作是否完成。然后在代码块之前,设置一个计时器以在超时后运行一段代码。计时器将检查代码块是否已完成执行,如果没有,则抛出异常。否则它什么也做不了。
当然,代码块的末尾应该将该字段设置为 true 以指示工作已完成。
【讨论】:
有一个非常简单的选项,还没有人提到:
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;
}
【讨论】: