一文帶你搞懂Java定時(shí)器Timer的使用
一、定時(shí)器是什么
定時(shí)器類似于我們生活中的鬧鐘,可以設(shè)定一個(gè)時(shí)間來提醒我們。
而定時(shí)器是指定一個(gè)時(shí)間去執(zhí)行一個(gè)任務(wù),讓程序去代替人工準(zhǔn)時(shí)操作。
標(biāo)準(zhǔn)庫(kù)中的定時(shí)器: Timer
方法 | 作用 |
---|---|
void schedule(TimerTask task, long delay) | 指定delay時(shí)間之后(單位毫秒)執(zhí)行任務(wù)task |
public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("定時(shí)器任務(wù)! "); } },1000); }
這段程序就是創(chuàng)建一個(gè)定時(shí)器,然后提交一個(gè)1000s后執(zhí)行的任務(wù)。
二、自定義定時(shí)器
我們自己實(shí)現(xiàn)一個(gè)定時(shí)器的前提是我們需要弄清楚定時(shí)器都有什么:
1.一個(gè)掃描線程,負(fù)責(zé)來判斷任務(wù)是否到時(shí)間需要執(zhí)行
2.需要有一個(gè)數(shù)據(jù)結(jié)構(gòu)來保存我們定時(shí)器中提交的任務(wù)
創(chuàng)建一個(gè)掃描線程相對(duì)比較簡(jiǎn)單,我們需要確定一個(gè)數(shù)據(jù)結(jié)構(gòu)來保存我們提交的任務(wù),我們提交過來的任務(wù),是由任務(wù)和時(shí)間組成的,我們需要構(gòu)建一個(gè)Task對(duì)象,數(shù)據(jù)結(jié)構(gòu)我們這里使用優(yōu)先級(jí)隊(duì)列,因?yàn)槲覀兊娜蝿?wù)是有時(shí)間順序的,具有一個(gè)優(yōu)先級(jí),并且要保證在多線程下是安全的,所以我們這里使用:PriorityBlockingQueue比較合適。
首先我們構(gòu)造一個(gè)Task對(duì)象
class MyTask { //即將執(zhí)行的任務(wù) private Runnable runnable; //在多久后執(zhí)行 private long time; public MyTask(Runnable runnable, long time) { this.runnable = runnable; this.time = time; } public long getTime() { return time; } //執(zhí)行任務(wù) public void run() { runnable.run(); } }
MyTimer類:
public class MyTimer { //掃描線程 private Thread t; //創(chuàng)建一個(gè)阻塞優(yōu)先級(jí)隊(duì)列,用來保存提交的Task對(duì)象 private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>(); private Object locker = new Object(); //提交任務(wù)的方法 public void schedule(Runnable runnable,long time) { //這里我們的時(shí)間換算一下,保存實(shí)際執(zhí)行的時(shí)間 MyTask task = new MyTask(runnable,System.currentTimeMillis() + time); queue.put(task); } //構(gòu)建掃描線程 public MyTimer() { t = new Thread(() -> { //我們?nèi)〕鲫?duì)列中時(shí)間最近的元素 while (true) { try { MyTask task = queue.take(); long curTime = System.currentTimeMillis(); if(curTime < task.getTime()) { //證明還沒到執(zhí)行的時(shí)間,再放進(jìn)隊(duì)列 queue.put(task); } else { //到時(shí)間了,執(zhí)行任務(wù) task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); } t.start(); }
雖然我們大體已經(jīng)寫出來了,但是我們這個(gè)定時(shí)器實(shí)現(xiàn)的還有一些問題。
問題1:既然我們是優(yōu)先級(jí)隊(duì)列,我們?cè)僮枞麅?yōu)先級(jí)隊(duì)列中放入Task對(duì)象時(shí),是根據(jù)什么建立堆的?
我們發(fā)現(xiàn)當(dāng)我們運(yùn)行程序時(shí),我們的程序也會(huì)報(bào)這樣的錯(cuò)誤。
class MyTask implements Comparable<MyTask>{ //即將執(zhí)行的任務(wù) private Runnable runnable; //在多久后執(zhí)行 private long time; public MyTask(Runnable runnable, long time) { this.runnable = runnable; this.time = time; } public long getTime() { return time; } //執(zhí)行任務(wù) public void run() { runnable.run(); } @Override public int compareTo(MyTask o) { return (int) (this.time - o.time); } }
我們需要實(shí)現(xiàn)Comparable接口并且重寫compareTo方法,指明我們是根據(jù)時(shí)間來決定在隊(duì)列中的優(yōu)先級(jí)。
2.我們的掃描線程,掃描的速度太快,造成了不必要的CPU資源浪費(fèi)。
比如我們?cè)缟?.00提交了一個(gè)中午12.00的任務(wù),那么我們這樣的程序就會(huì)從8.00一直循環(huán)幾十億次,而這樣的等待是沒有任何意義的。
更合理的方式是,不要在這里忙等,而是“阻塞式”等待。
public MyTimer() { t = new Thread(() -> { //我們?nèi)〕鲫?duì)列中時(shí)間最近的元素 while (true) { try { MyTask task = queue.take(); long curTime = System.currentTimeMillis(); if(curTime < task.getTime()) { //證明還沒到執(zhí)行的時(shí)間,再放進(jìn)隊(duì)列 queue.put(task); synchronized (locker) { locker.wait(task.getTime() - curTime); } } else { //到時(shí)間了,執(zhí)行任務(wù) task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); } t.start();
我們重寫一下掃描線程,進(jìn)行修改,當(dāng)我們判斷隊(duì)列中最近的一個(gè)任務(wù)的時(shí)間都沒到時(shí),我們的掃描線程就進(jìn)行阻塞等待,這里我們使用的不是wait(),而是wait(long time),我們傳入的參數(shù)是要執(zhí)行的時(shí)間和當(dāng)前時(shí)間的差值,有的同學(xué)可能會(huì)問了,那這樣執(zhí)行的時(shí)候和預(yù)期執(zhí)行的時(shí)間不就有出入了嘛?
因?yàn)槲覀兂绦蚶锏亩〞r(shí)操作,本來就難以做到非常準(zhǔn)確,因?yàn)椴僮飨到y(tǒng)調(diào)度是隨機(jī)的,有一定的時(shí)間開銷,存在ms的誤差都是相當(dāng)正常的,不影響我們的正常使用。
我們上面進(jìn)行阻塞等待,難道就傻傻的等到時(shí)間到了自動(dòng)喚醒嘛? 有沒有啥特殊情況呢?這里是有的,比如我們?cè)O(shè)定了一個(gè)阻塞到12點(diǎn)在喚醒,但我們又提交了一個(gè)10點(diǎn)的新任務(wù),那么我們就應(yīng)該提前喚醒了,所以我們應(yīng)該在每次提交任務(wù)后都進(jìn)行主動(dòng)喚醒,再由我們掃描線程決定是執(zhí)行還是繼續(xù)阻塞等待。
public void schedule(Runnable runnable,long time) { //這里我們的時(shí)間換算一下,保存實(shí)際執(zhí)行的時(shí)間 MyTask task = new MyTask(runnable,System.currentTimeMillis() + time); queue.put(task); synchronized (locker) { locker.notify(); } }
即使我們現(xiàn)在所有正常的情況都考慮到了,但是我們這里仍然存在一種極端的情況。
假設(shè)我們的掃描線程剛執(zhí)行完put方法,這個(gè)線程就被cpu調(diào)度走了,此時(shí)我們的另一個(gè)線程調(diào)用了schedule,添加了新任務(wù),新任務(wù)是10點(diǎn)執(zhí)行,然后notify,因?yàn)槲覀儾]有wait(),所以相當(dāng)于這里是空的notify,然后我們的線程調(diào)度回來去執(zhí)行wait()方法,但是我們的時(shí)間差仍然是之前算好的時(shí)間差,從8.00點(diǎn)到12.00點(diǎn),這樣就會(huì)產(chǎn)生很大的錯(cuò)誤。
這里造成這樣的問題,是因?yàn)槲覀兊膖ake操作和wait操作不是原子的,我們需要在take和wait之間加上鎖,保證每次notify的時(shí)候,都在wait中。
public MyTimer() { t = new Thread(() -> { while (true) { try { synchronized (locker) { MyTask Task = queue.take(); long curTime = System.currentTimeMillis(); if (curTime < Task.getTime()) { queue.put(Task); locker.wait(Task.getTime() - curTime); } else { Task.run(); } } } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start();
以上就是一文帶你搞懂Java定時(shí)器Timer的使用的詳細(xì)內(nèi)容,更多關(guān)于Java定時(shí)器Timer的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
import java和javax區(qū)別小結(jié)
Java包和javax包在Java編程語言中都起著至關(guān)重要的作用,本文就來介紹一下import java和javax區(qū)別小結(jié),具有一定的參考價(jià)值,感興趣的可以了解一下2024-10-10JSP服務(wù)器端和前端出現(xiàn)亂碼問題解決方案
這篇文章主要介紹了JSP服務(wù)器端和前端出現(xiàn)亂碼問題解決方案,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02關(guān)于Mybatis-plus設(shè)置字段為空的正確寫法
這篇文章主要介紹了關(guān)于Mybatis-plus設(shè)置字段為空的正確寫法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07SpringBoot枚舉類型參數(shù)認(rèn)證的實(shí)現(xiàn)代碼
項(xiàng)目當(dāng)中經(jīng)常需要接口參數(shù)是否在一個(gè)可選的范圍內(nèi),也就是驗(yàn)證類枚舉參數(shù)的需求,所以本文我們將使用SpringBoot實(shí)現(xiàn)枚舉類型參數(shù)認(rèn)證,文中有詳細(xì)的代碼示例,需要的朋友可以參考下2023-12-12Java對(duì)象簡(jiǎn)單實(shí)用案例之計(jì)算器實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Java對(duì)象簡(jiǎn)單實(shí)用案例之計(jì)算器實(shí)現(xiàn)代碼2016-11-11使用SpringBoot 配置Oracle和H2雙數(shù)據(jù)源及問題
這篇文章主要介紹了使用SpringBoot 配置Oracle和H2雙數(shù)據(jù)源及問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11java CompletableFuture實(shí)現(xiàn)異步編排詳解
這篇文章主要為大家介紹了java CompletableFuture實(shí)現(xiàn)異步編排詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01使用JavaWeb webSocket實(shí)現(xiàn)簡(jiǎn)易的點(diǎn)對(duì)點(diǎn)聊天功能實(shí)例代碼
這篇文章主要介紹了使用JavaWeb webSocket實(shí)現(xiàn)簡(jiǎn)易的點(diǎn)對(duì)點(diǎn)聊天功能實(shí)例代碼的相關(guān)資料,內(nèi)容介紹的非常詳細(xì),具有參考借鑒價(jià)值,感興趣的朋友一起學(xué)習(xí)吧2016-05-05