java中的任務(wù)調(diào)度之Timer定時(shí)器(案例和源碼分析)

定時(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芥玉。先給出一張整體類圖:

image

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ā)性欣除。 我們可以看一下圖:

image

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é)果:

image

正是Timer有很多的缺陷,所以出現(xiàn)了Timer的替代品ScheduledExecutorService尝苇,用來(lái)解決上面出現(xiàn)的問(wèn)題铛只。而且也出現(xiàn)了很多優(yōu)秀的框架。具體的我會(huì)在后續(xù)文章中介紹糠溜。

OK淳玩,今天的文章到這,歡迎批評(píng)指正非竿。

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凯肋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子汽馋,更是在濱河造成了極大的恐慌侮东,老刑警劉巖圈盔,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異悄雅,居然都是意外死亡驱敲,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)宽闲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)众眨,“玉大人,你說(shuō)我怎么就攤上這事容诬∶淅妫” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵览徒,是天一觀的道長(zhǎng)狈定。 經(jīng)常有香客問(wèn)我,道長(zhǎng)习蓬,這世上最難降的妖魔是什么纽什? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮躲叼,結(jié)果婚禮上芦缰,老公的妹妹穿的比我還像新娘。我一直安慰自己枫慷,他們只是感情好让蕾,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著或听,像睡著了一般探孝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上神帅,一...
    開(kāi)封第一講書(shū)人閱讀 51,562評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音萌抵,去河邊找鬼找御。 笑死,一個(gè)胖子當(dāng)著我的面吹牛绍填,可吹牛的內(nèi)容都是我干的霎桅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼讨永,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼滔驶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起卿闹,我...
    開(kāi)封第一講書(shū)人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤揭糕,失蹤者是張志新(化名)和其女友劉穎萝快,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體著角,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡揪漩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吏口。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奄容。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖产徊,靈堂內(nèi)的尸體忽然破棺而出昂勒,到底是詐尸還是另有隱情,我是刑警寧澤舟铜,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布戈盈,位于F島的核電站,受9級(jí)特大地震影響深滚,放射性物質(zhì)發(fā)生泄漏奕谭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一痴荐、第九天 我趴在偏房一處隱蔽的房頂上張望血柳。 院中可真熱鬧,春花似錦生兆、人聲如沸难捌。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)根吁。三九已至,卻和暖如春合蔽,著一層夾襖步出監(jiān)牢的瞬間击敌,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工拴事, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留沃斤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓刃宵,卻偏偏與公主長(zhǎng)得像衡瓶,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子牲证,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容