簡體   English   中英

Java:在某個代碼塊上設置超時?

[英]Java: set timeout on a certain block of code?

在某些代碼塊運行時間超過可接受的時間后,是否可以強制 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();

我將一些其他答案編譯成單個實用程序方法:

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!

是的,但強制另一個線程在隨機代碼行中斷通常是一個非常糟糕的主意。 如果您打算關閉該過程,則只會執行此操作。

你可以做的是在一段時間后對任務使用Thread.interrupt() 但是,除非代碼檢查它,否則它將無法工作。 使用Future.cancel(true)可以使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();
}

編輯: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. 在該方法中,假設它是循環而不是等待外部事件,添加本地字段並在每次循環周圍測試時間。

     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()不能直接返回值,它必須更新實例字段以返回其值。

我創建了一個非常簡單的解決方案,而不使用任何框架或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連接掛起或完全中斷。 這導致整個系統癱瘓。 我需要一種方法來檢測它並防止它發生。 所以我創建了這個並使用它。 效果很好。

而不是在主線程中的新線程和計時器中具有任務,而是在新線程中具有計時器並且在主線程中具有任務:

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

如果你想要一個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對該方法進行批注,以便在拋出異常時重試該方法三次。

我遇到了類似的問題,我的任務是在特定超時內將消息推送到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文檔說它確實可以保證執行被阻止。

“嘗試取消執行此任務。如果任務已經完成,已被取消或由於某些其他原因無法取消,則此嘗試將失敗。如果成功,並且在調用取消時此任務尚未啟動,則任務永遠不會運行。如果任務已經啟動,那么mayInterruptIfRunning參數確定執行此任務的線程是否應該被中斷以試圖停止任務。“

有一種hacky方式來做到這一點。

設置一些布爾字段以指示工作是否已完成。 然后在代碼塊之前,設置一個計時器以在超時后運行一段代碼。 計時器將檢查代碼塊是否已完成執行,如果沒有,則拋出異常。 否則它什么都不做。

當然,代碼塊的結尾應該將字段設置為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() ,但這可能會破壞您所依賴的任何同步機制。 請參閱其棄用通知

您還可以從線程中捕獲異常:

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

// ...

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

我也有這個問題,我的日志打印出''Unexpected end of stream''和''Could not get a resource from the pool'',我將brpop的超時設置為30s,redis為31s,mysql數據庫連接池到300s。 目前這個錯誤並沒有打印在log上,但是不知道以后會不會報這個錯誤,不知道對我寫數據庫有沒有不好的影響

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM