在 Java 應用中,常見需求是「每天凌晨 2 點同步數據」、「每小時刷新緩存」等定時任務。java.util.Timer 是 JDK 內置的輕量級調度工具,雖功能不如 Quartz/Spring Schedule 豐富,但**零依賴、API 簡單**,適合輕量級場景。本文通過完整案例講解 Timer 實現「每日固定時間執行」的關鍵細節與避坑指南。
TimerManager)import java.util.Calendar;
import java.util.Date;
import java.util.Timer;
public class TimerManager {
private static final long PERIOD_DAY = 24 * 60 * 60 * 1000; // 24小時毫秒數
public TimerManager() {
Calendar calendar = Calendar.getInstance();
// 1. 設置目標執行時間:每日 02:00:00
calendar.set(Calendar.HOUR_OF_DAY, 2);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0); // 關鍵:清零毫秒
Date firstRunTime = calendar.getTime();
// 2. 關鍵邏輯:若首次執行時間已過,順延至第二天
if (firstRunTime.before(new Date())) {
firstRunTime = addDays(firstRunTime, 1);
System.out.println("首次執行時間已過,調整為明天: " + firstRunTime);
} else {
System.out.println("首次執行時間: " + firstRunTime);
}
// 3. 啟動定時器
Timer timer = new Timer(true); // true = daemon 守護線程,JVM 退出時自動結束
TimerTask task = new MyDailyTask();
// schedule(task, firstTime, period) : 固定延遲執行 (上次執行結束後 period 再執行)
// scheduleAtFixedRate(task, firstTime, period): 固定頻率執行 (以首次時間為基準,無論執行耗時)
timer.schedule(task, firstRunTime, PERIOD_DAY);
}
// 輔助:日期加減天數
private Date addDays(Date date, int days) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.add(Calendar.DAY_OF_MONTH, days);
return cal.getTime();
}
public static void main(String[] args) {
new TimerManager(); // 測試啟動
}
}
TimerTask 實現)import java.util.TimerTask;
import java.util.logging.Logger;
public class MyDailyTask extends TimerTask {
private static final Logger LOG = Logger.getLogger(MyDailyTask.class.getName());
@Override
public void run() {
try {
// === 這裡編寫具體業務邏輯 ===
System.out.println("【定時任務】開始執行 - " + new java.util.Date());
// 例如: syncData(), clearCache(), sendReport()...
Thread.sleep(2000); // 模擬耗時操作
System.out.println("【定時任務】執行完成");
} catch (Exception e) {
LOG.severe("定時任務異常: " + e.getMessage());
// 關鍵:捕獲異常防止 Timer 線程終止導致後續任務不再執行!
}
}
}
ServletContextListener)import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener // Servlet 3.0+ 註解註冊,無需 web.xml
public class TimerListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("應用啟動,初始化定時任務...");
new TimerManager(); // 啟動調度
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("應用銷毀,定時任務隨守護線程自動停止。");
// 若 Timer 非 daemon,此處需手動 timer.cancel()
}
}
web.xml 傳統註冊方式 (Servlet < 3.0):
<listener>
<listener-class>com.yourpkg.TimerListener</listener-class>
</listener>
代碼核心:
if (firstRunTime.before(new Date())) {
firstRunTime = addDays(firstRunTime, 1);
}
timer.schedule 發現首次時間在過去,**會立即執行一次**,然後隔天 02:00 執行 — 極易導致業務重複執行、數據錯亂。| 方法 | 調度語義 | 適用場景 |
|---|---|---|
schedule(task, first, period) | 固定延遲:上次執行**結束**後 period 再執行 | 任務耗時波動大、不要求嚴格時間點、防止任務疊加 |
scheduleAtFixedRate(task, first, period) | 固定頻率:以首次時間為基準,**理論時間點**到達即執行 | 嚴格週期性 (如每秒采集)、任務耗時短且穩定 |
**日級任務通常用 schedule** (固定延遲),即使任務跑 1 分鐘,也不影響明天 02:00 準時啟動。
Timer 內部只有一個**單線程**輪詢任務隊列。若 run() 拋出未捕獲異常,**該線程會終止,導致所有後續定時任務永久停止**!
鐵律:run() 方法**必須包裹 try-catch(Exception),記錄日誌,決不拋出。
Timer timer = new Timer(true); // true = daemon
true 確保應用停止/重載時 Timer 線程不阻止 JVM 退出,配合 contextDestroyed 可選調用 timer.cancel() 釋放資源。| 局限 | 影響 | 替代方案 |
|---|---|---|
| 單線程串行執行 | 長任務阻塞後續任務 | ScheduledExecutorService (線程池) |
| 不支持持久化/集群 | 重啟丟失調度狀態、無法分佈式 | Quartz, XXL-JOB, Spring Schedule |
| 無彈性錯過處理 | 宕機期間任務無法補償 | Quartz Misfire 機制 |
| 時間表達式不靈活 | 只能固定間隔,無法「每月最後一天」等 | Cron 表達式支持框架 |
ScheduledExecutorService (JDK 內置,線程池,支持固定延遲/頻率)。Timer 以極簡 API 實現基礎定時調度,核心在於:**首次時間校正防誤觸發、run() 全異常捕獲防線程死掉、選對 schedule 模式**。它是零依賴輕量場景的不二之選。隨業務增長,及時遷移至 ScheduledExecutorService 或成熟調度框架,以獲得併發、持久化、運維能力。掌握 Timer 精髓,是通往高級調度架構的基石。