Java中的任務超時處理
綜述
任務超時處理是程式設計世界中一個不可或缺且常見的處理機制, 特別是在進行耗時任務時, 如網路請求, 資料庫查詢, 大規模資料處理等場景。在這些情況下, 為了防止任務因各種不可預測的因素(如網路延遲, 伺服器響應超時, 資料量過大等)而無休止地佔用寶貴的系統資源, 開發者通常會在任務執行邏輯中引入超時機制.
實現方式
在Java中,超時任務的處理主要透過異常處理、併發控制、執行緒管理等方式實現。例如,使用java.util.concurrent包中的Future介面和ExecutorService工具類,結合Callable介面和FutureTask,可以方便地設定超時任務。開發人員可以透過設定任務執行的超時時間,一旦任務執行超過設定的時間,系統將丟擲TimeoutException,通知開發者任務超時,從而及時採取相應的補救措施。
利用迴圈計算
利用迴圈檢查時間來處理超時是最直觀的思路, 但是這個做法如今幾乎沒什麼人在使用了. 原因有兩點, 首先雖然這個做法可以不使用執行緒/執行緒池, 但是"耗時操作通常不建議放在主執行緒中"這一點無論是後端專案還是在android專案中都基本屬於共識範疇, 所以仍然需要使用執行緒/執行緒池. 其次, 迴圈檢查無法處理因阻塞引起的超時, 這意味著直到執行緒從阻塞恢復才能執行判斷是否退出的語句.
所以無論從效能還是安全性來判斷使用 ExecutorService 提供的 api 來實現才是更好的選擇. 此處給出示例僅供瞭解.
public class TimeoutTask {
private ExecutorService executorService;
private TimeoutTask() {
this.executorService = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors() * 2,
10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
private static class SingletonHolder {
private static final TimeoutTask instance = new TimeoutTask();
}
//單例模式
public static TimeoutTask getInstance() {
return SingletonHolder.instance;
}
private static String formatTime(long time) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
return sdf.format(new Date(time));
}
public void timeoutWithLoop(long millisecondLimit) {
executorService.execute(new Runnable() {
@Override
public void run() {
long startTime = System.currentTimeMillis();
for (; ; ) {
var currentTime = System.currentTimeMillis();
System.out.println("當前的時間: " + formatTime(currentTime));
//這裡使用thread.sleep模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (currentTime - startTime > millisecondLimit) {
//為了方便測試程式碼執行結束自動關閉額外使用了shutdown
executorService.shutdown();
break;
}
}
}
});
}
}
public class Main {
public static void main(String[] args) {
TimeoutTask.getInstance().timeoutWithLoop(4000);
}
}
另外, 透過迴圈處理超時任務還有一個變種做法, 那就是透過 Timer 或者其他延時api控制某個 AtomicBoolean 格式的flag進行轉變, 在迴圈中透過檢查flag來判斷是否超時並跳出. 這裡額外再提一句, 示例就不給出了.
當然, 這個變種做法依然存在此前提到的兩種問題, 同樣不推薦, 僅供瞭解.
利用 future.get() 和 future.cancel() 方法
這應該是當前處理非同步任務超時情況下最好的處理方式, 透過 executorService.submit() 獲取 future , 透過 future.get() 設定超時期限, 再在 TimeoutException 觸發時透過 future.cancel() 取消超時任務.
public class TimeoutTask {
private ExecutorService executorService;
private TimeoutTask() {
this.executorService = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors() * 2,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
private static class SingletonHolder {
private static final TimeoutTask instance = new TimeoutTask();
}
//單例模式
public static TimeoutTask getInstance() {
return SingletonHolder.instance;
}
private static String formatTime(long time) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
return sdf.format(new Date(time));
}
public void timeoutWithFuture(long targetTime) {
Future<?> future = executorService.submit(() -> {
try {
TimeUnit.MILLISECONDS.sleep(8000);
var awaitEndTime = System.currentTimeMillis();
System.out.println("透過condition.await()等待結束時間: "+ formatTime(awaitEndTime) + "這一行不應該被列印");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
try {
var startTime = System.currentTimeMillis();
System.out.println("超時示例開始時間: "+ formatTime(startTime));
future.get(targetTime, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
// 超時處理
var endTime = System.currentTimeMillis();
System.out.println("任務按照預想超時結束, 結束時間: "+ formatTime(endTime));
future.cancel(true);
} catch (ExecutionException | InterruptedException e) {
System.out.println("報錯資訊: "+e.getMessage());
} finally {
executorService.shutdown();
}
}
}
總結
綜上所述, Java中的任務超時處理是一個非常重要且常見的程式設計實踐. 處理這個問題時, 利用Java併發包中的Future介面和ExecutorService工具類基本可以被看作常規場景下的最優選. 這一做法簡單有效. 另外, 在中型以上的專案中還可以進一步擴充, 將超時處理封裝成類, 透過自定義的方法, 類似 executeWithTimeout(callable timeOutCallback, long timeInMillisecond, runnable onTimeOut){} 這樣的封裝. 這樣可以不需要每次使用時都寫一大堆套路程式碼, 減小工作量.