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

個(gè)人學(xué)習(xí)筆記分享,當(dāng)前能力有限,請(qǐng)勿貶低,菜鳥(niǎo)互學(xué),大佬繞道

如有勘誤,歡迎指出和討論,本文后期也會(huì)進(jìn)行修正和補(bǔ)充


前言

定時(shí)器顧名思義,即定時(shí)觸發(fā)某個(gè)事件,分離開(kāi)來(lái),即包含三個(gè)因素:==定時(shí)==,==觸發(fā)==,==某個(gè)事件==,本文也將以此為基礎(chǔ)介紹五種常見(jiàn)的定時(shí)器


本文只做基于SpringBoot的示例,其余版本的請(qǐng)自行查閱資料,大同小異


1.介紹

1.1.目的

定時(shí)器的目的即為了在某個(gè)時(shí)間點(diǎn),程序自身主動(dòng)觸發(fā)某個(gè)事件,而不需要外力去開(kāi)啟或者啟動(dòng),以節(jié)省人力并統(tǒng)一管理

1.2.示例場(chǎng)景

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

是不是覺(jué)得很常見(jiàn)?

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

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


==本文僅簡(jiǎn)述前3種,比較簡(jiǎn)單易懂,quartz會(huì)專(zhuān)門(mén)分離出來(lái)整理==


2.@Scheduled注解

2.1.介紹:

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

2.2.步驟:

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

2.3.注解:

  • 注解@Scheduled為方法注解,用于標(biāo)記某個(gè)方法在何時(shí)定時(shí)執(zhí)行
  • 需要配合另一個(gè)注解@EnableScheduling進(jìn)行使用,該注解用于標(biāo)記某個(gè)類(lèi),開(kāi)啟定時(shí)任務(wù),通常標(biāo)記在定時(shí)器所在的類(lèi),或者直接設(shè)置在項(xiàng)目啟動(dòng)類(lèi)上

2.4.@Scheduled參數(shù):

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

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

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

  • fixedDelayString、fixedRateString、initialDelayString:與上訴三種作用一直,但參數(shù)為字符串類(lèi)型,因而可以使用占位符,形如

    @Scheduled(fixedDelayString = "${time.fixedDelay}")
    
  • @Scheduled(cron = "0 0,30 0,8 ? * ? "):方法在每天的8點(diǎn)30分0秒執(zhí)行,參數(shù)為字符串類(lèi)型,那么同理也可使用占位符,cron表達(dá)式請(qǐng)另行查閱資料,推薦看這篇文章:http://www.itdecent.cn/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ì):簡(jiǎn)單便捷,僅兩行注解便完成了定時(shí)效果
  • 劣勢(shì):所有參數(shù)和執(zhí)行的方法必須提前寫(xiě)入代碼里,可擴(kuò)展性極低


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

3.1.樣例

使用非常簡(jiǎn)單,這里先給出樣例,在對(duì)照進(jìn)行介紹

代碼如下

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自帶的工具類(lèi),代碼量分別為721行和162行(包括注釋?zhuān)疾欢?,有興趣的可以直接看看源碼

3.2.1.TimerTask

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

3.2.2.Timer

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


實(shí)際上包括三個(gè)類(lèi):

  • Timer:即定時(shí)器主類(lèi),負(fù)責(zé)管理所有的定時(shí)任務(wù),每個(gè)Timer擁有一個(gè)私有的TaskQueueTimerThread,

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

    TaskQueue實(shí)質(zhì)上只有一個(gè)長(zhǎng)度為128的數(shù)組用于存儲(chǔ)TimerTask、一個(gè)int型變量size表示隊(duì)列長(zhǎng)度、以及對(duì)這兩個(gè)數(shù)據(jù)的增刪改查

  • TimerThread:即定時(shí)器線程,線程會(huì)共享TaskQueue里面的數(shù)據(jù),TimerThread會(huì)對(duì)TaskQueue里的任務(wù)進(jìn)行消耗

    TimerThread實(shí)際上就是一個(gè)Thread線程,會(huì)不停的監(jiān)聽(tīng)TaskQueue,如果隊(duì)列里面有任務(wù),那么就執(zhí)行第一個(gè),并將其刪除(先刪除再執(zhí)行)


流程分析

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

這么一看,這不就是典型的==生產(chǎn)者/消費(fèi)者模式==,timer負(fù)責(zé)生產(chǎn)(實(shí)際上是接受),而TimerThread負(fù)責(zé)消費(fèi),TaskQueue作為中轉(zhuǎn)倉(cāng)庫(kù)


構(gòu)造方法

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

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

public Timer(String name, boolean isDaemon)
  • name:即線程名,用于區(qū)分不同的線程,缺省的時(shí)候默認(rèn)使用"Timer-" + serialNumber()生成唯一線程名
  • isDaemon:是否是守護(hù)線程,缺省的時(shí)候默認(rèn)為否,有啥區(qū)別請(qǐng)自行了解,有機(jī)會(huì)的話我也會(huì)整理筆記


核心方法

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

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

    • schedule(TimerTask task, long delay):指定任務(wù)task,在delay毫秒延遲后執(zhí)行
    • schedule(TimerTask task, Date time):指定任務(wù)task,在time時(shí)間點(diǎn)執(zhí)行一次
    • schedule(TimerTask task, long delay, long period):指定任務(wù)task,延遲delay毫秒后執(zhí)行第一次,并在之后每隔period毫秒執(zhí)行一次
    • schedule(TimerTask task, Date firstTime, long period):指定任務(wù)task,在firstTime的時(shí)候執(zhí)行第一次,之后每隔period毫秒執(zhí)行一次
    • scheduleAtFixedRate(TimerTask task, long delay, long period):作用與schedule一致
    • scheduleAtFixedRate(TimerTask task, Date firstTime, long period):作用與schedule一致

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

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

    scheduleAtFixedRate使用本次預(yù)計(jì)時(shí)間計(jì)算下一次,即time + period

    ==對(duì)于耗時(shí)任務(wù),兩者區(qū)別較大,請(qǐng)按需求選擇,瞬時(shí)任務(wù)無(wú)區(qū)別==

  • 取消任務(wù)方法:cancel(),會(huì)將任務(wù)隊(duì)列清空,并堵塞線程,且不再能夠接受任務(wù)(接受時(shí)報(bào)錯(cuò)),并不會(huì)銷(xiāo)毀本身的實(shí)例和其內(nèi)部的線程

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

補(bǔ)充

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

    任務(wù)隊(duì)列會(huì)在每一次添加任務(wù)和刪除任務(wù)時(shí),進(jìn)行堆排序矯正,凈化也會(huì)對(duì)剩余任務(wù)重新堆排序

  • cancel的時(shí)候線程如何處理

    定時(shí)器線程進(jìn)行堵塞處理,并沒(méi)有銷(xiāo)毀,在執(zhí)行當(dāng)前任務(wù)后就不會(huì)執(zhí)行下一次了,但是==線程并沒(méi)有銷(xiāo)毀==

    所以盡量不要?jiǎng)?chuàng)建太多timer對(duì)象,會(huì)增加服務(wù)器負(fù)擔(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)然可以縮寫(xiě)為樣例里面的寫(xiě)法,更加簡(jiǎn)潔,請(qǐng)按照自己需求修改

4.線程

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

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,這里給出常用的幾個(gè)示例

  • 設(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ā)時(shí)間為每天凌晨1點(diǎn)

    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

專(zhuān)門(mén)整理了一篇quartz的筆記,有興趣的可以看我上一篇博客

寫(xiě)的并不完善,后續(xù)應(yīng)該會(huì)進(jìn)行修正

6.總結(jié)

  • @schedule使用方便快捷,但功能有限,擴(kuò)展性極低,適用于不需要統(tǒng)一管理的簡(jiǎn)單場(chǎng)景
  • Timer可以統(tǒng)一管理定時(shí)任務(wù),但自身作為一個(gè)工具類(lèi),功能較少,但是也適用于很多場(chǎng)景了
  • 線程的使用同樣比較方便,靈活度特別高,支持各種類(lèi)型的觸發(fā)時(shí)間,但畢竟沒(méi)有專(zhuān)用的框架,功能并不算特別齊全,適用于對(duì)自由度要求較高的場(chǎng)景
  • quartz作為專(zhuān)門(mén)的定時(shí)器項(xiàng)目,功能齊全且強(qiáng)大,目前大部分項(xiàng)目仍只使用了其小部分功能,適用于要求較高的場(chǎng)景


7.demo地址

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

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

僅實(shí)現(xiàn)了部分功能作為樣例,請(qǐng)按照需求自己擴(kuò)展哦,有疑問(wèn)或者建議歡迎聯(lián)系我~


BB兩句

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

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



作者:Echo_Ye

WX:Echo_YeZ

EMAIL :echo_yezi@qq.com

個(gè)人站點(diǎn):在搭了在搭了。。。(右鍵 - 新建文件夾)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容