【JAVA定時器】四種常見定時器的原理和簡單實現(xiàn)

個人學(xué)習(xí)筆記分享,當(dāng)前能力有限贡茅,請勿貶低秘蛇,菜鳥互學(xué),大佬繞道

如有勘誤顶考,歡迎指出和討論赁还,本文后期也會進行修正和補充


前言

定時器顧名思義,即定時觸發(fā)某個事件驹沿,分離開來艘策,即包含三個因素:==定時==,==觸發(fā)==渊季,==某個事件==朋蔫,本文也將以此為基礎(chǔ)介紹五種常見的定時器


本文只做基于SpringBoot的示例,其余版本的請自行查閱資料却汉,大同小異


1.介紹

1.1.目的

定時器的目的即為了在某個時間點驯妄,程序自身主動觸發(fā)某個事件,而不需要外力去開啟或者啟動合砂,以節(jié)省人力并統(tǒng)一管理

1.2.示例場景

  • 管理系統(tǒng)青扔,需要每日12點將前一天的數(shù)據(jù)進行備份,并生成歷史數(shù)據(jù)統(tǒng)計
  • 宿管系統(tǒng),每日10點將所有未歸人員統(tǒng)計出來赎懦,主動交由管理人員
  • 硬件設(shè)備,需要每隔2分鐘檢查設(shè)備是否連接正常幻工,設(shè)備異常需要更新狀態(tài)到管理端励两,必要時通知有關(guān)人員
  • 圖書館借書管理系統(tǒng),每天12點需要檢查即將超時和已超時歸還的書籍囊颅,并通過短信或其他途徑通知有關(guān)人員
  • 手機下載管理系統(tǒng)当悔,開啟下載后每隔0.5s刷新一次下載進度,在下載完成或者長時間卡頓時告知用戶
  • 訂單管理系統(tǒng)踢代,用戶下達訂單后開需要在半小時內(nèi)付款盲憎,成功付款則生成訂單結(jié)果,超時未付款則自動取消訂單

是不是覺得很常見胳挎?

1.3.常見實現(xiàn)方案

  • @Scheduled注解:基于注解
  • Timer().schedule創(chuàng)建任務(wù):基于封裝類Timer
  • 線程:使用線程直接執(zhí)行任務(wù)即可饼疙,可以與thread、線程池慕爬、ScheduleTask等配合使用
  • quartz配置定時器:基于springquartz框架


==本文僅簡述前3種窑眯,比較簡單易懂,quartz會專門分離出來整理==


2.@Scheduled注解

2.1.介紹:

使用注解標(biāo)記需要定時執(zhí)行的方法医窿,并設(shè)置執(zhí)行時間磅甩,便可使其在指定的時間執(zhí)行指定方法

2.2.步驟:

  1. 使用注解@Scheduled標(biāo)記目標(biāo)方法,參數(shù)為執(zhí)行時間
  2. 使用注解@EnableScheduling標(biāo)記目標(biāo)方法所在的類姥卢,或者直接標(biāo)記項目啟動類

2.3.注解:

  • 注解@Scheduled為方法注解卷要,用于標(biāo)記某個方法在何時定時執(zhí)行
  • 需要配合另一個注解@EnableScheduling進行使用,該注解用于標(biāo)記某個類独榴,開啟定時任務(wù)僧叉,通常標(biāo)記在定時器所在的類,或者直接設(shè)置在項目啟動類上

2.4.@Scheduled參數(shù):

  • @Scheduled(fixedDelay = 5000):方法執(zhí)行完成后等待5秒再次執(zhí)行

  • @Scheduled(fixedRate = 5000):方法每隔5秒執(zhí)行一次

  • @Scheduled(initialDelay=1000, fixedRate=5000):延遲1秒后執(zhí)行第一次括眠,之后每隔5秒執(zhí)行一次

  • fixedDelayStringfixedRateString捞烟、initialDelayString:與上訴三種作用一直题画,但參數(shù)為字符串類型苍息,因而可以使用占位符,形如

    @Scheduled(fixedDelayString = "${time.fixedDelay}")
    
  • @Scheduled(cron = "0 0,30 0,8 ? * ? "):方法在每天的8點30分0秒執(zhí)行表谊,參數(shù)為字符串類型爆办,那么同理也可使用占位符距辆,cron表達式請另行查閱資料跨算,推薦看這篇文章:http://www.reibang.com/p/1defb0f22ed1

2.5.示例

示例1:每隔3秒執(zhí)行一次

@Component
@EnableScheduling
public class ScheduleTest {

    private int count = 0;

    /**
     * 每3秒鐘執(zhí)行一次
     */
    @Scheduled(cron = "*/3 * * * * ?")
    public void test1() {
        System.out.println(count + ":" + (new Date()).toString());
        count++;
    }
}
image-20200907123608014

示例2:第一次等待10秒诸蚕,之后每3秒一次

@Component
@EnableScheduling
public class ScheduleTest {

    private int count = 0;

    /**
     * 第一次等待10秒挫望,之后每3秒鐘執(zhí)行一次
     */
    @Scheduled(initialDelay = 10000, fixedRate = 3000)
    public void test1() {
        System.out.println(count + ":" + (new Date()).toString());
        count++;
    }

}
image-20200907124022947

2.6.小結(jié)

  • 優(yōu)勢:簡單便捷狂窑,僅兩行注解便完成了定時效果
  • 劣勢:所有參數(shù)和執(zhí)行的方法必須提前寫入代碼里泉哈,可擴展性極低


3.Timer().schedule創(chuàng)建任務(wù)

3.1.樣例

使用非常簡單,這里先給出樣例奕纫,在對照進行介紹

代碼如下

package com.yezi_tool.demo_basic.test;

import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

@Component
public class TimerTest {
    private Integer count = 0;

    public TimerTest() {
        testTimer();
    }

    public void testTimer() {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    //do Something
                    System.out.println(new Date().toString() + ": " + count);
                    count++;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, 0, 1000);
    }
}

執(zhí)行結(jié)果

image-20210225092748479

可以看到每隔1s打印一次count并自增1


3.2.介紹

核心包括Timer和TimerTask,均為jkd自帶的工具類升筏,代碼量分別為721行和162行(包括注釋)您访,都不多灵汪,有興趣的可以直接看看源碼

3.2.1.TimerTask

TimerTask實際上就是一個Runnable而已享言,繼承Runnable并添加了幾個自定義的參數(shù)和方法,沒啥好介紹的蔚晨,有興趣可以看源碼

3.2.2.Timer

Timer字面意思即定時器肛循,為jkd自帶的工具類多糠,提供定時執(zhí)行任務(wù)的相關(guān)功能


實際上包括三個類:

  • Timer:即定時器主類夹孔,負責(zé)管理所有的定時任務(wù)搭伤,每個Timer擁有一個私有的TaskQueueTimerThread

  • TaskQueue:即任務(wù)隊列袜瞬,Timer生產(chǎn)任務(wù),然后推到TaskQueue里存放邓尤,等待處理拍鲤,被處理掉的任務(wù)即被移除掉

    TaskQueue實質(zhì)上只有一個長度為128的數(shù)組用于存儲TimerTask汞扎、一個int型變量size表示隊列長度季稳、以及對這兩個數(shù)據(jù)的增刪改查

  • TimerThread:即定時器線程澈魄,線程會共享TaskQueue里面的數(shù)據(jù)景鼠,TimerThread會對TaskQueue里的任務(wù)進行消耗

    TimerThread實際上就是一個Thread線程,會不停的監(jiān)聽TaskQueue痹扇,如果隊列里面有任務(wù)铛漓,那么就執(zhí)行第一個鲫构,并將其刪除(先刪除再執(zhí)行)


流程分析

  • Timer生產(chǎn)任務(wù)(實際上是從外部接收到任務(wù))票渠,并將任務(wù)推到TaskQueue里面存放问顷,并喚醒TaskQueue線程(queue.notify()
  • TimerThread監(jiān)聽TaskQueue肠骆,若里面有任務(wù)則將其執(zhí)行并移除隊里,若沒有任務(wù)則讓隊列等待(queue.wait()

這么一看扫外,這不就是典型的==生產(chǎn)者/消費者模式==莉钙,timer負責(zé)生產(chǎn)(實際上是接受),而TimerThread負責(zé)消費筛谚,TaskQueue作為中轉(zhuǎn)倉庫


構(gòu)造方法

構(gòu)造的時候會設(shè)置定時器線程的名字并將其啟動

完整格式如下磁玉,其中兩個參數(shù)均可缺省

public Timer(String name, boolean isDaemon)
  • name:即線程名,用于區(qū)分不同的線程驾讲,缺省的時候默認使用"Timer-" + serialNumber()生成唯一線程名
  • isDaemon:是否是守護線程蚊伞,缺省的時候默認為否,有啥區(qū)別請自行了解吮铭,有機會的話我也會整理筆記


核心方法

核心方法有添加任務(wù)时迫、取消任務(wù)和凈化三種

  • 添加任務(wù)有6中公用方法(實際最后使用同一種私有方法)

    • schedule(TimerTask task, long delay):指定任務(wù)task,在delay毫秒延遲后執(zhí)行
    • schedule(TimerTask task, Date time):指定任務(wù)task谓晌,在time時間點執(zhí)行一次
    • schedule(TimerTask task, long delay, long period):指定任務(wù)task掠拳,延遲delay毫秒后執(zhí)行第一次,并在之后每隔period毫秒執(zhí)行一次
    • schedule(TimerTask task, Date firstTime, long period):指定任務(wù)task纸肉,在firstTime的時候執(zhí)行第一次碳想,之后每隔period毫秒執(zhí)行一次
    • scheduleAtFixedRate(TimerTask task, long delay, long period):作用與schedule一致
    • scheduleAtFixedRate(TimerTask task, Date firstTime, long period):作用與schedule一致

    實際上最后都會使用sched(TimerTask task, long time, long period),即指定任務(wù)task毁靶,在time執(zhí)行第一次胧奔,之后每隔period毫秒執(zhí)行一次

    schedule使用系統(tǒng)時間計算下一次,即System.currentTimeMillis()+period

    scheduleAtFixedRate使用本次預(yù)計時間計算下一次预吆,即time + period

    ==對于耗時任務(wù)龙填,兩者區(qū)別較大,請按需求選擇拐叉,瞬時任務(wù)無區(qū)別==

  • 取消任務(wù)方法:cancel()岩遗,會將任務(wù)隊列清空,并堵塞線程凤瘦,且不再能夠接受任務(wù)(接受時報錯)宿礁,并不會銷毀本身的實例和其內(nèi)部的線程

  • 凈化方法:purge(),凈化會將隊列里所有被取消的任務(wù)移除蔬芥,對剩余任務(wù)進行堆排序梆靖,并返回移除任務(wù)的數(shù)量

補充

  • 如何保證第一個任務(wù)是執(zhí)行時間最早的

    任務(wù)隊列會在每一次添加任務(wù)和刪除任務(wù)時控汉,進行堆排序矯正,凈化也會對剩余任務(wù)重新堆排序

  • cancel的時候線程如何處理

    定時器線程進行堵塞處理返吻,并沒有銷毀姑子,在執(zhí)行當(dāng)前任務(wù)后就不會執(zhí)行下一次了,但是==線程并沒有銷毀==

    所以盡量不要創(chuàng)建太多timer對象测僵,會增加服務(wù)器負擔(dān)

3.3.使用步驟

  1. 初始化Timer

    Timer timer=new Timer();
    
  2. 初始化task

    private class MyTask extends TimerTask {
            @Override
            public void run() {
                try {
                    //do Something
                    System.out.println(new Date().toString() + ": " + count);
                    count++;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    MyTask myTask=new MyTask();
    
  3. 添加任務(wù)

    timer.schedule(myTask, 5000, 3000);
    

    完整代碼:

    package com.yezi_tool.demo_basic.test;
    
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    import java.util.Timer;
    import java.util.TimerTask;
    
    @Component
    public class TimerTest {
        private Integer count = 0;
    
        public TimerTest() {
            testTimer2();
        }
    
        public void testTimer2() {
            Timer timer = new Timer();
            MyTask myTask = new MyTask();
            timer.schedule(myTask, 0, 1000);
        }
    
        private class MyTask extends TimerTask {
            @Override
            public void run() {
                try {
                    //do Something
                    System.out.println(new Date().toString() + ": " + count);
                    count++;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
    }
    

    當(dāng)然可以縮寫為樣例里面的寫法街佑,更加簡潔,請按照自己需求修改

4.線程

線程應(yīng)該是最常見的實現(xiàn)方案捍靠,創(chuàng)建一個線程執(zhí)行任務(wù)即可沐旨,舉例幾個不同的寫法,代碼如下

4.1.使用thread + runnable

package com.yezi_tool.demo_basic.test;

import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class ThreadTest {

    private Integer count = 0;

    public ThreadTest() {
        test1();
    }

    public void test1() {
        new Thread(() -> {
            while (count < 10) {
                System.out.println(new Date().toString() + ": " + count);
                count++;
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

4.2.使用線程池 + runnable

package com.yezi_tool.demo_basic.test;

import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Component
public class ThreadTest {

    private static final ExecutorService threadPool = Executors.newFixedThreadPool(5);// 線程池
    private Integer count = 0;

    public ThreadTest() {
        test2();
    }

    public void test2() {
        threadPool.execute(() -> {
            while (count < 10) {
                System.out.println(new Date().toString() + ": " + count);
                count++;
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

4.3.使用ScheduledTask + runnable

ScheduledTask 有11種添加任務(wù)的方法榨婆,詳情直接查看文件TaskScheduler.java磁携,這里給出常用的幾個示例

  • 設(shè)置觸發(fā)頻率為3000毫秒

    package com.yezi_tool.demo_basic.test;
    
    import org.springframework.scheduling.TaskScheduler;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    
    @Component
    public class ThreadTest {
    
        private Integer count = 0;
        private final TaskScheduler taskScheduler;
    
        public ThreadTest(TaskScheduler taskScheduler) {
            this.taskScheduler = taskScheduler;
            test3();
        }
    
        public void test3() {
            taskScheduler.scheduleAtFixedRate(() -> {
                System.out.println(new Date().toString() + ": " + count);
                count++;
            }, 3000);
        }
    }
    
  • 設(shè)置觸發(fā)時間為每天凌晨1點

    package com.yezi_tool.demo_basic.test;
    
    import org.springframework.scheduling.TaskScheduler;
    import org.springframework.scheduling.support.CronTrigger;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    
    @Component
    public class ThreadTest {
    
        private Integer count = 0;
        private final TaskScheduler taskScheduler;
    
        public ThreadTest(TaskScheduler taskScheduler) {
            this.taskScheduler = taskScheduler;
            test4();
        }
    
        public void test4() {
            taskScheduler.schedule(() -> {
                System.out.println(new Date().toString() + ": " + count);
                count++;
            }, new CronTrigger("0 0 1 * * ?"));
        }
    }
    

5.quartz

專門整理了一篇quartz的筆記,有興趣的可以看我上一篇博客

寫的并不完善纲辽,后續(xù)應(yīng)該會進行修正

6.總結(jié)

  • @schedule使用方便快捷,但功能有限璃搜,擴展性極低拖吼,適用于不需要統(tǒng)一管理的簡單場景
  • Timer可以統(tǒng)一管理定時任務(wù),但自身作為一個工具類这吻,功能較少吊档,但是也適用于很多場景了
  • 線程的使用同樣比較方便,靈活度特別高唾糯,支持各種類型的觸發(fā)時間怠硼,但畢竟沒有專用的框架,功能并不算特別齊全移怯,適用于對自由度要求較高的場景
  • quartz作為專門的定時器項目香璃,功能齊全且強大,目前大部分項目仍只使用了其小部分功能舟误,適用于要求較高的場景


7.demo地址

https://gitee.com/echo_ye/demo_basic/tree/scheduleDemo

不同定時器啟用方法在README.MD中查看葡秒,一共6種方法,如有紕漏請聯(lián)系我

僅實現(xiàn)了部分功能作為樣例嵌溢,請按照需求自己擴展哦眯牧,有疑問或者建議歡迎聯(lián)系我~


BB兩句

其實除了@schedule,其余的都可以自定義管理器赖草,來統(tǒng)一管理学少,并動態(tài)修改,具體咋做此處先不做贅述

quartz已經(jīng)整理除了靜態(tài)定時器和動態(tài)定時器秧骑,有興趣的可以瞅瞅



作者:Echo_Ye

WX:Echo_YeZ

EMAIL :echo_yezi@qq.com

個人站點:在搭了在搭了版确。扣囊。。(右鍵 - 新建文件夾)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末阀坏,一起剝皮案震驚了整個濱河市如暖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌忌堂,老刑警劉巖盒至,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異士修,居然都是意外死亡枷遂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進店門棋嘲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酒唉,“玉大人,你說我怎么就攤上這事沸移』韭祝” “怎么了?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵雹锣,是天一觀的道長网沾。 經(jīng)常有香客問我,道長蕊爵,這世上最難降的妖魔是什么辉哥? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮攒射,結(jié)果婚禮上醋旦,老公的妹妹穿的比我還像新娘。我一直安慰自己会放,他們只是感情好饲齐,可當(dāng)我...
    茶點故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著咧最,像睡著了一般箩张。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上窗市,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天先慷,我揣著相機與錄音,去河邊找鬼咨察。 笑死论熙,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的摄狱。 我是一名探鬼主播脓诡,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼无午,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了祝谚?” 一聲冷哼從身側(cè)響起宪迟,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎交惯,沒想到半個月后次泽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡席爽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年意荤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片只锻。...
    茶點故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡玖像,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出齐饮,到底是詐尸還是另有隱情捐寥,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布祖驱,位于F島的核電站握恳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏羹膳。R本人自食惡果不足惜睡互,卻給世界環(huán)境...
    茶點故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一根竿、第九天 我趴在偏房一處隱蔽的房頂上張望陵像。 院中可真熱鬧,春花似錦寇壳、人聲如沸醒颖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泞歉。三九已至,卻和暖如春匿辩,著一層夾襖步出監(jiān)牢的瞬間腰耙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工铲球, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留挺庞,地道東北人。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓稼病,卻偏偏與公主長得像选侨,于是被迫代替她去往敵國和親掖鱼。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,870評論 2 361