Java定時任務-Timer
1. Timer簡單使用
簡單使用:
public class TimerTest { public static void main(String[] args) throws InterruptedException { // 建立Timer物件 Timer timer = new Timer(); // 延遲1秒執行任務,只執行一次。 timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello"); } }, 1000); Thread.sleep(2000); timer.cancel(); // 取消任務執行,如果沒有呼叫cancel,timer執行緒不會結束。 } }
TimerTask介面繼承了Runnable,重寫裡面的run方法就可以自定義任務。
2. Timer
Timer類中,有兩個重要欄位:
/** * The timer task queue. This data structure is shared with the timer * thread. The timer produces tasks, via its various schedule calls, * and the timer thread consumes, executing timer tasks as appropriate, * and removing them from the queue when they're obsolete. */ private final TaskQueue queue = new TaskQueue(); /** * The timer thread. */ private final TimerThread thread = new TimerThread(queue);
TaskQueue用來儲存提交的定時任務的佇列;TimerThread是Timer的內部類,繼承了Thread類。
TaskQueue的底層實現是陣列,它是一個優先佇列。
private TimerTask[] queue = new TimerTask[128];
優先順序參考下一次執行時間(每一個TimerTask下一次的執行時間),越快該執行的任務就會排的越靠前。
TimerThread是一個執行緒,當Timer建立時,該執行緒就被啟動:
public Timer(String name) { thread.setName(name); thread.start(); }
3. 配置定時任務
主要用Timer中的schedule方法來配置不同的定時任務。
- schedule(TimerTask task, long delay):在delay(毫秒)延遲後,執行任務task。只執行一次。
- schedule(TimerTask task, Date time):在指定的時間點(time)執行task。如果時間點是過去某個時刻,那麼該任務將被立即執行。
- schedule(TimerTask task, long delay, long period):在當前時間delay(毫秒)延遲後執行task,然後每隔period(毫秒)執行一次
- schedule(TimerTask task, Date firstTime, long period):在指定時間firstTime執行第一次task,然後每隔period(毫秒)執行一次。
- scheduleAtFixedRate(TimerTask task, long delay, long period):在delay延遲後執行task,每隔period執行一次
- scheduleAtFixedRate(TimerTask task, Date firstTime, long period):在指定時間執行task,每隔period執行一次
schedule(TimerTask task, long delay, long period) 與 scheduleAtFixedRate(TimerTask task, long delay, long period)的區別:
兩者都是在當前時間的delay延遲後,執行第一次任務,並且之後每隔period執行一次。
區別在於:當任務的執行時間 > 任務間間隔時間(period)時,
- schedule方法會阻塞,直到上一個任務執行完畢後,再執行下一個任務;
- scheduleAtFixedRate方法不會阻塞,上一個任務即使未執行完畢,只要間隔時間足夠period,就執行下一個任務。
在演示前,需要搞明白兩個時間變數:
系統時間:正常流逝的時間(System.currentTimeMillis()或new Date())。
任務被執行的時間(TimerTask中的scheduledExecutionTime()方法)。
示例:schedule
public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
timer.schedule(new TimerTask() {
@Override
public void run() {
// 我們在任務中分別列印“任務被執行時間”和當前系統時間
System.out.println("scheuledExecutionTime : " + dateFormat.format(new Date(this.scheduledExecutionTime())));
System.out.println("new Date() : " + dateFormat.format(new Date()));
try {
// 阻塞6秒,超過了任務間隔4秒
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 2000, 4000); // 指定任務執行間隔4秒
}
列印結果:
scheuledExecutionTime : 2020-08-23 13:49:33
new Date() : 2020-08-23 13:49:33
scheuledExecutionTime : 2020-08-23 13:49:39
new Date() : 2020-08-23 13:49:39
scheuledExecutionTime : 2020-08-23 13:49:45
new Date() : 2020-08-23 13:49:45
scheuledExecutionTime : 2020-08-23 13:49:51
new Date() : 2020-08-23 13:49:51
scheuledExecutionTime : 2020-08-23 13:49:57
new Date() : 2020-08-23 13:49:57
可以看到,任務執行時間(scheduleExecutionTime)和系統時間的間隔都是6秒。
任務執行的實際間隔是6秒,也就是上一個任務結束後,下一個任務才執行,並未按照設定的4秒間隔。
示例:scheduleAtFixedRate
public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("scheuledExecutionTime : " + dateFormat.format(new Date(this.scheduledExecutionTime())));
System.out.println("new Date() : " + dateFormat.format(new Date()));
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 2000, 4000);
}
列印結果:
scheuledExecutionTime : 2020-08-23 14:31:54
new Date() : 2020-08-23 14:31:54
scheuledExecutionTime : 2020-08-23 14:31:58
new Date() : 2020-08-23 14:32:00
scheuledExecutionTime : 2020-08-23 14:32:02
new Date() : 2020-08-23 14:32:06
scheuledExecutionTime : 2020-08-23 14:32:06
new Date() : 2020-08-23 14:32:12
scheuledExecutionTime : 2020-08-23 14:32:10
new Date() : 2020-08-23 14:32:18
可以看到:任務被執行的時間(scheduleExecutionTime)的間隔是4秒,而列印的系統時間間隔是6秒。也就是說,任務的執行時間間隔按照設定的4秒進行,並未因上一個任務的阻塞而導致間隔延長。
但是注意:“scheuledExecutionTime”和“new Date() ”兩句話都是同時列印的,都是在一個task開始時被列印。
schedule(TimerTask task, Date firstTime, long period) 與 scheduleAtFixedRate(TimerTask task, Date firstTime, long period)的區別:
這兩個方法的區別主要在於:當firstTime早於當前系統時間時,執行的情況不同。
- schedule:會立即執行task一次,之後每隔period執行一次;
- scheduleAtFixedRate:會將從firstTime到當前系統時間(System.currentTimeMillis())之間應該執行的任務一次性執行完畢,然後繼續按照period間隔執行。
示例:schedule
public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 當前時間
System.out.printf("=============當前時間: %s=================\n", dateFormat.format(new Date()));
// 從當前時間的20秒前開始執行任務
Date startDate = Date.from(Instant.now().minusSeconds(20));
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("scheuledExecutionTime : " + dateFormat.format(new Date(this.scheduledExecutionTime())));
}
}, startDate, 4000); // 每隔4秒執行一次
}
列印結果:
=============當前時間: 2020-08-23 14:45:05=================
scheuledExecutionTime : 2020-08-23 14:45:05
scheuledExecutionTime : 2020-08-23 14:45:09
scheuledExecutionTime : 2020-08-23 14:45:13
scheuledExecutionTime : 2020-08-23 14:45:17
scheuledExecutionTime : 2020-08-23 14:45:21
可以看到,任務是從當前時間開始執行的,並未從startDate這個時間點開始。
示例:scheduleAtFixedRate
public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 當前時間
System.out.printf("=============當前時間: %s=================\n", dateFormat.format(new Date()));
// 從當前時間的20秒前開始執行任務
Date startDate = Date.from(Instant.now().minusSeconds(20));
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("scheuledExecutionTime : " + dateFormat.format(new Date(this.scheduledExecutionTime())));
}
}, startDate, 4000);
}
列印結果:
=============當前時間: 2020-08-23 14:47:30=================
scheuledExecutionTime : 2020-08-23 14:47:10
scheuledExecutionTime : 2020-08-23 14:47:14
scheuledExecutionTime : 2020-08-23 14:47:18
scheuledExecutionTime : 2020-08-23 14:47:22
scheuledExecutionTime : 2020-08-23 14:47:26
scheuledExecutionTime : 2020-08-23 14:47:30
scheuledExecutionTime : 2020-08-23 14:47:34
scheuledExecutionTime : 2020-08-23 14:47:38
scheuledExecutionTime : 2020-08-23 14:47:42
可以看到:scheduleAtFixedRate方法將從開始時間(startDate 14:47:10)到當前時間之間(new Date() 14:47:30)之間的所有任務都執行一遍(這些任務都是同時執行,一次性輸出的),然後從當前時間起繼續以4秒為間隔往下執行。
4. 底層實現
每一個schedule方法的底層都是呼叫了sched方法。
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;
synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}
queue.add(task);
if (queue.getMin() == task)
queue.notify();
}
}
該方法接收3個引數:
- task:執行的任務;
- time:任務下一次要執行的時間點;
- period:執行任務的時間間隔。
當我們構造 Timer 例項的時候,就會啟動該執行緒,該執行緒會在一個死迴圈中嘗試從任務佇列上獲取任務,如果成功獲取就執行該任務並在執行結束之後做一個判斷。
如果 period 值為零,則說明這是一次普通任務,執行結束後將從佇列首部移除該任務。
如果 period 為負值,則說明這是一次固定延時的任務,修改它下次執行時間 nextExecutionTime 為當前時間減去 period,重構任務佇列。
如果 period 為正數,則說明這是一次固定頻率的任務,修改它下次執行時間為 上次執行時間加上 period,並重構任務佇列。
- Timer的不足
Timer是單執行緒的,不管任務多少,僅有一個工作執行緒;
限於單執行緒,如果第一個任務邏輯上死迴圈了,後續的任務一個都得不到執行。
依然是由於單執行緒,任一任務丟擲異常後,整個 Timer 就會結束,後續任務全部都無法執行。