【JAVA定時器】四種常見定時器的原理和簡單實現


個人學習筆記分享,當前能力有限,請勿貶低,菜鳥互學,大佬繞道

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


前言

定時器顧名思義,即定時觸發某個事件,分離開來,即包含三個因素:定時觸發某個事件,本文也將以此為基礎介紹五種常見的定時器


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


1.介紹

1.1.目的

定時器的目的即為了在某個時間點,程序自身主動觸發某個事件,而不需要外力去開啟或者啟動,以節省人力並統一管理

1.2.示例場景

  • 管理系統,需要每日12點將前一天的數據進行備份,並生成歷史數據統計
  • 宿管系統,每日10點將所有未歸人員統計出來,主動交由管理人員
  • 硬件設備,需要每隔2分鍾檢查設備是否連接正常,設備異常需要更新狀態到管理端,必要時通知有關人員
  • 圖書館借書管理系統,每天12點需要檢查即將超時和已超時歸還的書籍,並通過短信或其他途徑通知有關人員
  • 手機下載管理系統,開啟下載后每隔0.5s刷新一次下載進度,在下載完成或者長時間卡頓時告知用戶
  • 訂單管理系統,用戶下達訂單后開需要在半小時內付款,成功付款則生成訂單結果,超時未付款則自動取消訂單

是不是覺得很常見?

1.3.常見實現方案

  • @Scheduled注解:基於注解
  • Timer().schedule創建任務:基於封裝類Timer
  • 線程:使用線程直接執行任務即可,可以與thread、線程池、ScheduleTask等配合使用
  • quartz配置定時器:基於springquartz框架

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


2.@Scheduled注解

2.1.介紹:

使用注解標記需要定時執行的方法,並設置執行時間,便可使其在指定的時間執行指定方法

2.2.步驟:

  1. 使用注解@Scheduled標記目標方法,參數為執行時間
  2. 使用注解@EnableScheduling標記目標方法所在的類,或者直接標記項目啟動類

2.3.注解:

  • 注解@Scheduled為方法注解,用於標記某個方法在何時定時執行
  • 需要配合另一個注解@EnableScheduling 進行使用,該注解用於標記某個類,開啟定時任務,通常標記在定時器所在的類,或者直接設置在項目啟動類上

2.4.@Scheduled參數:

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

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

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

  • fixedDelayStringfixedRateStringinitialDelayString:與上訴三種作用一直,但參數為字符串類型,因而可以使用占位符,形如

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

2.5.示例

示例1:每隔3秒執行一次

@Component
@EnableScheduling
public class ScheduleTest {

    private int count = 0;

    /**
     * 每3秒鍾執行一次
     */
    @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秒鍾執行一次
     */
    @Scheduled(initialDelay = 10000, fixedRate = 3000)
    public void test1() {
        System.out.println(count + ":" + (new Date()).toString());
        count++;
    }

}

image-20200907124022947

2.6.小結

  • 優勢:簡單便捷,僅兩行注解便完成了定時效果
  • 劣勢:所有參數和執行的方法必須提前寫入代碼里,可擴展性極低

3.Timer().schedule創建任務

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);
    }
}

執行結果

image-20210225092748479

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


3.2.介紹

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

3.2.1.TimerTask

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

3.2.2.Timer

Timer字面意思即定時器,為jkd自帶的工具類,提供定時執行任務的相關功能


實際上包括三個類:

  • Timer:即定時器主類,負責管理所有的定時任務,每個Timer擁有一個私有的TaskQueueTimerThread

  • TaskQueue:即任務隊列,Timer生產任務,然后推到TaskQueue里存放,等待處理,被處理掉的任務即被移除掉

    TaskQueue實質上只有一個長度為128的數組用於存儲TimerTask、一個int型變量size表示隊列長度、以及對這兩個數據的增刪改查

  • TimerThread:即定時器線程,線程會共享TaskQueue里面的數據,TimerThread會對TaskQueue里的任務進行消耗

    TimerThread實際上就是一個Thread線程,會不停的監聽TaskQueue,如果隊列里面有任務,那么就執行第一個,並將其刪除(先刪除再執行)


流程分析

  • Timer生產任務(實際上是從外部接收到任務),並將任務推到TaskQueue里面存放,並喚醒TaskQueue線程(queue.notify()
  • TimerThread監聽TaskQueue,若里面有任務則將其執行並移除隊里,若沒有任務則讓隊列等待(queue.wait()

這么一看,這不就是典型的生產者/消費者模式timer負責生產(實際上是接受),而TimerThread負責消費,TaskQueue作為中轉倉庫


構造方法

構造的時候會設置定時器線程的名字並將其啟動

完整格式如下,其中兩個參數均可缺省

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

核心方法

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

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

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

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

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

    scheduleAtFixedRate使用本次預計時間計算下一次,即time + period

    對於耗時任務,兩者區別較大,請按需求選擇,瞬時任務無區別

  • 取消任務方法:cancel(),會將任務隊列清空,並堵塞線程,且不再能夠接受任務(接受時報錯),並不會銷毀本身的實例和其內部的線程

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

補充

  • 如何保證第一個任務是執行時間最早的

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

  • cancel的時候線程如何處理

    定時器線程進行堵塞處理,並沒有銷毀,在執行當前任務后就不會執行下一次了,但是線程並沒有銷毀

    所以盡量不要創建太多timer對象,會增加服務器負擔

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. 添加任務

    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();
                }
            }
        }
    
    }
    

    當然可以縮寫為樣例里面的寫法,更加簡潔,請按照自己需求修改

4.線程

線程應該是最常見的實現方案,創建一個線程執行任務即可,舉例幾個不同的寫法,代碼如下

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種添加任務的方法,詳情直接查看文件TaskScheduler.java,這里給出常用的幾個示例

  • 設置觸發頻率為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);
        }
    }
    
  • 設置觸發時間為每天凌晨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的筆記,有興趣的可以看我上一篇博客

寫的並不完善,后續應該會進行修正

6.總結

  • @schedule使用方便快捷,但功能有限,擴展性極低,適用於不需要統一管理的簡單場景
  • Timer可以統一管理定時任務,但自身作為一個工具類,功能較少,但是也適用於很多場景了
  • 線程的使用同樣比較方便,靈活度特別高,支持各種類型的觸發時間,但畢竟沒有專用的框架,功能並不算特別齊全,適用於對自由度要求較高的場景
  • quartz作為專門的定時器項目,功能齊全且強大,目前大部分項目仍只使用了其小部分功能,適用於要求較高的場景

7.demo地址

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

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

僅實現了部分功能作為樣例,請按照需求自己擴展哦,有疑問或者建議歡迎聯系我~


BB兩句

其實除了@schedule,其余的都可以自定義管理器,來統一管理,並動態修改,具體咋做此處先不做贅述

quartz已經整理除了靜態定時器和動態定時器,有興趣的可以瞅瞅



作者:Echo_Ye

WX:Echo_YeZ

EMAIL :echo_yezi@qq.com

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM