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