定時(shí)器相信大家都不陌生,平時(shí)使用定時(shí)器就像使用鬧鐘一樣,我們可以在固定的時(shí)間做某件事翅溺,也可以在固定的時(shí)間段重復(fù)做某件事,今天就來(lái)分析一下java中自帶的定時(shí)任務(wù)器Timer髓抑。
一咙崎、Timer基本使用
在Java中為我們提供了Timer來(lái)實(shí)現(xiàn)定時(shí)任務(wù),當(dāng)然現(xiàn)在還有很多定時(shí)任務(wù)框架吨拍,比如說(shuō)Spring褪猛、QuartZ、Linux Cron等等密末,而且性能也更加優(yōu)越握爷。但是我們想要深入的學(xué)習(xí)就必須先從最簡(jiǎn)單的開(kāi)始。
在Timer定時(shí)任務(wù)中严里,最主要涉及到了兩個(gè)類:Timer和TimerTask新啼。他們倆的關(guān)系也特別容易理解,TimerTask把我們得業(yè)務(wù)邏輯寫(xiě)好之后刹碾,然后使用Timer定時(shí)執(zhí)行就OK了燥撞。我們來(lái)看一個(gè)最基本的案例:
public class MyTimerTask extends TimerTask {
private String taskName;
public MyTimerTask(String taskName) {
this.taskName = taskName;
}
public String getTaskName() {
return taskName;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println("當(dāng)前執(zhí)行的任務(wù)是:" + taskName);
}
}
這就是我們的TimerTask,我們單獨(dú)寫(xiě)成類時(shí)候需要去繼承TimerTask迷帜。然后呢我們寫(xiě)好了之后就可以使用Timer來(lái)執(zhí)行了物舒。
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
MyTimerTask myTask = new MyTimerTask("TimerTask 1");
//在2秒鐘之后執(zhí)行第一次,之后每隔一秒執(zhí)行一次
timer.schedule(myTask, 2000L, 1000L);
}
}
指定的流程很簡(jiǎn)單:
(1)第一步:創(chuàng)建一個(gè)Timer戏锹。
(2)第二步:創(chuàng)建一個(gè)TimerTask冠胯。
(3)第三步:使用Timer執(zhí)行TimerTask。
其中第三步無(wú)疑是我們目前最關(guān)心的锦针,也就是timer.schedule(myTask, 2000L, 1000L)荠察。他的意思是myTask在兩秒鐘之后開(kāi)始第一次執(zhí)行置蜀,然后每隔一秒執(zhí)行一次。這只是最基本的用法悉盆。就體現(xiàn)了Timer定時(shí)執(zhí)行的流程盯荤。當(dāng)然java中Timer還為我們提供了很多其他的方法。對(duì)此就有必要深入其源碼看看了焕盟。
二秋秤、Timer源碼分析
對(duì)于一個(gè)類的源碼分析,我一貫的思路就是先從參數(shù)開(kāi)始脚翘,然后構(gòu)造方法灼卢,最后就是常用方法。下面我們就按照這個(gè)思路開(kāi)始今天的源碼分析来农,在這里基于jdk1.8芥玉。先給出一張整體類圖:
1、參數(shù)
Timer的源碼中為我們提供了兩個(gè)最主要的參數(shù)TaskQueue和TimerThread备图。
/**
* 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);
上面的代碼大概意思是這樣的:
(1)TaskQueue:這是一個(gè)最小堆,它存放該Timer的所有TimerTask赶袄。
(2)TimerThread:執(zhí)行TaskQueue中的任務(wù)揽涮,執(zhí)行完從任務(wù)隊(duì)列中移除。
所以上面這兩個(gè)參數(shù)其實(shí)是配合著使用的饿肺,那這個(gè)TaskQueue是如何存放的呢蒋困?在這里我們不妨跟進(jìn)去看看。
class TaskQueue {
/**
* Priority queue represented as a balanced binary heap: the two children
* of queue[n] are queue[2*n] and queue[2*n+1]. The priority queue is
* ordered on the nextExecutionTime field: The TimerTask with the lowest
* nextExecutionTime is in queue[1] (assuming the queue is nonempty). For
* each node n in the heap, and each descendant of n, d,
* n.nextExecutionTime <= d.nextExecutionTime.
*/
private TimerTask[] queue = new TimerTask[128];
//增刪改查的方法
}
在這里我們只給出了一部分源碼敬辣,不過(guò)這一部分是整個(gè)思想原理最核心的雪标,上面英文的大概意思是;TaskQueue是一個(gè)平衡二叉堆,具有最小 nextExecutionTime 的 TimerTask 在隊(duì)列中為 queue[1] 溉跃,也就是堆中的根節(jié)點(diǎn)村刨。第 n 個(gè)位置 queue[n] 的子節(jié)點(diǎn)分別在 queue[2n] 和 queue[2n+1] 。不了解二叉堆的話撰茎,可以看看數(shù)據(jù)結(jié)構(gòu)嵌牺。
也就是說(shuō)TimerTask 在堆中的位置其實(shí)是通過(guò)nextExecutionTime 來(lái)決定的。nextExecutionTime 越小龄糊,那么在堆中的位置越靠近根逆粹,越有可能先被執(zhí)行。而nextExecutionTime意思就是下一次執(zhí)行開(kāi)始的時(shí)間炫惩。
還有一個(gè)TimerTask數(shù)組僻弹,默認(rèn)大小是128個(gè)。
2他嚷、構(gòu)造方法
構(gòu)造方法就比較簡(jiǎn)單了蹋绽,這里一共有四個(gè):
public Timer() {
this("Timer-" + serialNumber());
}
public Timer(boolean isDaemon) {
this("Timer-" + serialNumber(), isDaemon);
}
public Timer(String name) {
thread.setName(name);
thread.start();
}
public Timer(String name, boolean isDaemon) {
thread.setName(name);
thread.setDaemon(isDaemon);
thread.start();
}
(1)第一個(gè):默認(rèn)構(gòu)造方法芭毙。
(2)第二個(gè):在構(gòu)造器中指定是否是守護(hù)線程。
(3)第三個(gè):帶有名字的構(gòu)造方法蟋字。
(3)第四個(gè):不僅帶名字稿蹲,還指定是否是守護(hù)線程。
不過(guò)我們需要注意一點(diǎn)的是鹊奖,Timer在構(gòu)造完成之后會(huì)啟動(dòng)一個(gè)后臺(tái)線程用于執(zhí)行TaskQueue里面的TimerTask 苛聘。
3、定時(shí)任務(wù)方法
在一開(kāi)始我們提到忠聚,我們不僅可以在指定的時(shí)間執(zhí)行某些任務(wù)设哗,還可以在一段時(shí)間之后執(zhí)行。我們對(duì)這些方法進(jìn)行總結(jié)一下:
(1)schedule(task,time) 在時(shí)間等于或超過(guò)time的時(shí)候執(zhí)行且只執(zhí)行一次task两蟀,這個(gè)time表示的是例如2019年11月11日上午11點(diǎn)11分11秒网梢。指的是時(shí)刻。
(2)schedule(task,time,period)
在時(shí)間等于或超過(guò)time的時(shí)候首次執(zhí)行task赂毯,之后每隔period毫秒重復(fù)執(zhí)行一次task 。這個(gè)time和上一個(gè)一樣党涕。
(3)schedule(task, delay)
在delay時(shí)間之后烦感,執(zhí)行且只執(zhí)行一次task。這個(gè)delay表示的是延遲時(shí)間膛堤,比如說(shuō)三秒后執(zhí)行手趣。
(4)schedule(task,delay,period)
在delay時(shí)間之后,開(kāi)始首次執(zhí)行task肥荔,之后每隔period毫秒重復(fù)執(zhí)行一次task 绿渣,這個(gè)delay和上面的一樣。
我們不如來(lái)看看源碼:
//第一個(gè):
public void schedule(TimerTask task, Date time) {
sched(task, time.getTime(), 0);
}
//第二個(gè):
public void schedule(TimerTask task, Date firstTime, long period) {
if (period <= 0) throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), -period);
}
//第三個(gè)
public void schedule(TimerTask task, long delay) {
if (delay < 0) throw new IllegalArgumentException("Negative delay.");
sched(task, System.currentTimeMillis()+delay, 0);
}
//第四個(gè):
public void schedule(TimerTask task, long delay, long period) {
if (delay < 0) throw new IllegalArgumentException("Negative delay.");
if (period <= 0) throw new IllegalArgumentException("Non-positive period.");
sched(task, System.currentTimeMillis()+delay, -period);
}
這四個(gè)方法都執(zhí)行了同一個(gè)方法sched燕耿,所以我們要弄清楚原理中符,就必須要再跟進(jìn)去看看:
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
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();
}
}
上面的代碼我們來(lái)分析一下,最上面的if就是排除一下異常情況誉帅,最核心的就是synchronized里面的代碼舟茶。首先將任務(wù)添加到隊(duì)列中,然后根據(jù)nextExecutionTime調(diào)整隊(duì)列堵第。
添加任務(wù)add(task):
void add(TimerTask task) {
if (size + 1 == queue.length)
//添加任務(wù)很簡(jiǎn)單吧凉,就是數(shù)組的拷貝
queue = Arrays.copyOf(queue, 2*queue.length);
queue[++size] = task;
fixUp(size);//維護(hù)最小堆
}
維護(hù)最小堆:
private void fixUp(int k) {
while (k > 1) {
int j = k >> 1;
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
上面就是Timer中如何執(zhí)行的定時(shí)任務(wù)核心,但是還有一個(gè)方法踏志,也是執(zhí)行定時(shí)任務(wù)的阀捅。叫scheduleAtFixedRate
下面我們來(lái)分析一下,然后比較和上面的不同针余。
4饲鄙、scheduleAtFixedRate方法
這個(gè)方法有兩個(gè):
(1)scheduleAtFixedRate(task, time, period)
在時(shí)間等于或超過(guò)time的時(shí)候首次執(zhí)行task凄诞,之后每隔period毫秒重復(fù)執(zhí)行一次task 。這個(gè)time表示的是例如2019年11月11日上午11點(diǎn)11分11秒忍级。指的是時(shí)刻帆谍。
(2)scheduleAtFixedRate(task, delay, period)
在delay時(shí)間之后,開(kāi)始首次執(zhí)行task轴咱,之后每隔period毫秒重復(fù)執(zhí)行一次task 汛蝙,這個(gè)delay表示的是延遲時(shí)間,比如說(shuō)三秒后執(zhí)行朴肺。
既然上面都已經(jīng)有了4個(gè)定時(shí)器窖剑,為什么這里還要再增加幾個(gè)呢?我們來(lái)分析一下他們的區(qū)別:
分兩種情況: ① 首次計(jì)劃執(zhí)行的時(shí)間 schedule:如果第一次執(zhí)行時(shí)間被delay了戈稿,隨后的執(zhí)行時(shí)間按照上一次實(shí)際執(zhí)行完的時(shí)間點(diǎn)進(jìn)行計(jì)算 西土。 scheduleAtFixedRate:如果第一次執(zhí)行時(shí)間被delay了,隨后的執(zhí)行時(shí)間按上一次開(kāi)始的時(shí)間進(jìn)行計(jì)算鞍盗,并且為了趕上進(jìn)度會(huì)多次執(zhí)行任務(wù)需了,因此TimerTask中的執(zhí)行體需要考慮同步。
②任務(wù)執(zhí)行所需時(shí)間 schedule方法:下一次執(zhí)行時(shí)間會(huì)不斷延后般甲,因此參照的是上一次執(zhí)行完成的時(shí)間點(diǎn)援所。 scheduleAtFixedRate方法:下一次執(zhí)行時(shí)間不會(huì)延后,因此存在并發(fā)性欣除。 我們可以看一下圖:
5、其他方法
我們已經(jīng)明白了如何創(chuàng)建Timer和執(zhí)行定時(shí)任務(wù)挪略,如果在執(zhí)行的時(shí)候我們突然改變主意历帚,想要取消怎么辦呢?這里Timer當(dāng)然為我們提供了杠娱。
(1)cancel:取消此計(jì)時(shí)器任務(wù)挽牢。
(2)scheduledExecutionTime():返回此任務(wù)最近實(shí)際執(zhí)行的安排執(zhí)行時(shí)間。
6摊求、任務(wù)調(diào)度
任務(wù)調(diào)度也就是說(shuō)我們的線程如何去執(zhí)行這些任務(wù)禽拔。其實(shí)在TimerThread調(diào)用了run來(lái)執(zhí)行,我們看一下源碼室叉。
public void run() {
try {
mainLoop();
} finally {
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear();
}
}
}
也就是說(shuō)其實(shí)真正執(zhí)行任務(wù)調(diào)度的是mainLoop()睹栖,synchronized代碼塊只是為了確保在執(zhí)行完之后能夠移除這個(gè)task。
而這個(gè)mainLoop方法的思想很簡(jiǎn)單茧痕,就是拿出任務(wù)隊(duì)列中的第一個(gè)任務(wù)野来,如果執(zhí)行時(shí)間還沒(méi)有到,則繼續(xù)等待踪旷,否則立即執(zhí)行曼氛。源碼在這里就不再給出了豁辉。
三、Timer缺陷
上面從源碼的角度分析了一下Timer舀患,因?yàn)橛梅ê芎?jiǎn)單徽级,主要是源碼分析。說(shuō)了這么多聊浅,Timer還是有一定的缺陷的餐抢,
1、Timer管理延時(shí)任務(wù)的缺陷
Timer在執(zhí)行定時(shí)任務(wù)時(shí)只會(huì)創(chuàng)建一個(gè)線程狗超,所以如果存在多個(gè)任務(wù)弹澎,且任務(wù)時(shí)間過(guò)長(zhǎng),超過(guò)了兩個(gè)任務(wù)的間隔時(shí)間努咐,會(huì)發(fā)生一些缺陷苦蒿。我們看一個(gè)例子:
這個(gè)例子中的功能是這樣的,第一個(gè)任務(wù)在1秒鐘之后開(kāi)始執(zhí)行渗稍,第二個(gè)任務(wù)在2秒鐘之后開(kāi)始執(zhí)行佩迟。
第一步:定義兩個(gè)TimerTask
public class Task1 extends TimerTask {
@Override
public void run() {
try {
SimpleDateFormat sdf =
new SimpleDateFormat("hh:MM:ss");
String data = sdf.format(new Date());
System.out.println("task 1:"+data);
//休眠了3秒
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
還有一個(gè):
public class Task2 extends TimerTask {
@Override
public void run() {
SimpleDateFormat sdf =
new SimpleDateFormat("hh:MM:ss");
String data = sdf.format(new Date());
System.out.println("task 2:"+data);
}
}
第二步:我們測(cè)試一下:
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
Task1 myTask1 = new Task1();
Task2 myTask2 = new Task2();
SimpleDateFormat sdf =
new SimpleDateFormat("hh:MM:ss");
String data = sdf.format(new Date());
System.out.println("Main :"+data);
timer.schedule(myTask1, 1000L);
timer.schedule(myTask2, 2000L);
}
}
//Main :05:09:30
//task 1:05:09:31
//task 2:05:09:34
我們?cè)谏厦娴腡ask1中會(huì)發(fā)現(xiàn),任務(wù)2不是應(yīng)該在32秒的時(shí)候執(zhí)行嘛竿屹,怎么會(huì)在4秒鐘之后才執(zhí)行报强。究其原因是任務(wù)1執(zhí)行了3秒,但是線程只有一個(gè)拱燃,所以只能先把任務(wù)1執(zhí)行完才去執(zhí)行任務(wù)2秉溉。這就是其缺陷之一。
2碗誉、Timer當(dāng)任務(wù)拋出異常時(shí)的缺陷
這個(gè)缺陷的意思是召嘶,其中有一個(gè)任務(wù)拋出了RuntimeException,那么所有的任務(wù)都會(huì)停止執(zhí)行哮缺。這個(gè)演示起來(lái)很簡(jiǎn)單弄跌。
第一步:聲明幾個(gè)定時(shí)任務(wù)
public class Task1 extends TimerTask {
@Override
public void run() {
throw new RuntimeException();
}
}
public class Task2 extends TimerTask {
@Override
public void run() {
System.out.println("任務(wù)2執(zhí)行");
}
}
第二步:測(cè)試
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
Task1 myTask1 = new Task1();
Task2 myTask2 = new Task2();
timer.schedule(myTask1, 1000L);
timer.schedule(myTask2, 2000L);
}
}
我們來(lái)看一下結(jié)果:
正是Timer有很多的缺陷,所以出現(xiàn)了Timer的替代品ScheduledExecutorService尝苇,用來(lái)解決上面出現(xiàn)的問(wèn)題铛只。而且也出現(xiàn)了很多優(yōu)秀的框架。具體的我會(huì)在后續(xù)文章中介紹糠溜。
OK淳玩,今天的文章到這,歡迎批評(píng)指正非竿。