Java中的任務超時處理

地维藏光發表於2024-10-07

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){} 這樣的封裝. 這樣可以不需要每次使用時都寫一大堆套路程式碼, 減小工作量.

相關文章