您好,登錄后才能下訂單哦!
這篇文章主要講解了“Java多線程如何實現定時器”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Java多線程如何實現定時器”吧!
定時器是一種實際開發中非常常用的組件, 類似于一個 “鬧鐘”, 達到一個設定的時間之后, 就執行某個指定好的代碼.
比如網絡通信中, 如果對方 500ms 內沒有返回數據, 則斷開連接嘗試重連.
比如一個 Map, 希望里面的某個 key 在 3s 之后過期(自動刪除).
類似于這樣的場景就需要用到定時器.
標準庫中提供了一個 Timer 類, Timer 類的核心方法為schedule.
Timer類構造時內部會創建線程, 有下面的四個構造方法, 可以指定線程名和是否將定時器內部的線程指定為后臺線程(即守護線程), 如果不指定, 定時器對象內部的線程默認為前臺線程.
序號 | 構造方法 | 解釋 |
---|---|---|
1 | public Timer() | 無參, 定時器關聯的線程為前臺線程, 線程名為默認值 |
2 | public Timer(boolean isDaemon) | 指定定時器中關聯的線程類型, true(后臺線程), false(前臺線程) |
3 | public Timer(String name) | 指定定時器關聯的線程名, 線程類型為前臺線程 |
4 | public Timer(String name, boolean isDaemon) | 指定定時器關聯的線程名和線程類型 |
schedule 方法是給Timer注冊一個任務, 這個任務在指定時間后進行執行, TimerTask類就是專門描述定時器任務的一個抽象類, 它實現了Runnable接口.
public abstract class TimerTask implements Runnable // jdk源碼
序號 | 方法 | 解釋 |
---|---|---|
1 | public void schedule(TimerTask task, long delay) | 指定任務, 延遲多久執行該任務 |
2 | public void schedule(TimerTask task, Date time) | 指定任務, 指定任務的執行時間 |
3 | public void schedule(TimerTask task, long delay, long period) | 連續執行指定任務, 延遲時間, 連續執行任務的時間間隔, 毫秒為單位 |
4 | public void schedule(TimerTask task, Date firstTime, long period) | 連續執行指定任務, 第一次任務的執行時間, 連續執行任務的時間間隔 |
5 | public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) | 與方法4作用相同 |
6 | public void scheduleAtFixedRate(TimerTask task, long delay, long period) | 與方法3作用相同 |
7 | public void cancel() | 清空任務隊列中的全部任務, 正在執行的任務不受影響 |
代碼示例:
import java.util.Timer; import java.util.TimerTask; public class TestProgram { public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("執行延后3s的任務!"); } }, 3000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("執行延后2s后的任務!"); } }, 2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("執行延后1s的任務!"); } }, 1000); } }
執行結果:
觀察執行結果, 任務執行結束后程序并沒有結束, 即進程并沒有結束, 這是因為上面的代碼定時器內部是開啟了一個線程去執行任務的, 雖然任務執行完成了, 但是該線程并沒有銷毀; 這和自己定義一個線程執行完成 run 方法后就自動銷毀是不一樣的, Timer 本質上是相當于線程池, 它緩存了一個工作線程, 一旦任務執行完成, 該工作線程就處于空閑狀態, 等待下一輪任務.
首先, 我們需要定義一個類, 用來描述一個定時器當中的任務, 類要成員要有一個Runnable, 再加上一個任務執行的時間戳, 具體還包含如下內容:
構造方法, 用來指定任務和任務的延遲執行時間.
兩個get方法, 分別用來給外部對象獲取該對象的任務和執行時間.
實現Comparable接口, 指定比較方式, 用于判斷定時器任務的執行順序, 每次需要執行時間最早的任務.
class MyTask implements Comparable<MyTask>{ //要執行的任務 private Runnable runnable; //任務的執行時間 private long time; public MyTask(Runnable runnable, long time) { this.runnable = runnable; this.time = time; } //獲取當前任務的執行時間 public long getTime() { return this.time; } //執行任務 public void run() { runnable.run(); } @Override public int compareTo(MyTask o) { return (int) (this.time - o.time); } }
然后就需要實現定時器類了, 我們需要使用一個數據結構來組織定時器中的任務, 需要每次都能將時間最早的任務找到并執行, 這個情況我們可以考慮用優先級隊列(即小根堆)來實現, 當然我們還需要考慮線程安全的問題, 所以我們選用優先級阻塞隊列 PriorityBlockingQueue 是最合適的, 特別要注意在自定義的任務類當中要實現比較方式, 或者實現一下比較器也行.
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
我們自己實現的定時器類中要有一個注冊任務的方法, 用來將任務插入到優先級阻塞隊列中;
還需要有一個線程用來執行任務, 這個線程是從優先級阻塞隊列中取出隊首任務去執行, 如果這個任務還沒有到執行時間, 那么線程就需要把這個任務再放會隊列當中, 然后線程就進入等待狀態, 線程等待可以使用sleep和wait, 但這里有一個情況需要考慮, 當有新任務插入到隊列中時, 我們需要喚醒線程重新去優先級阻塞隊列拿隊首任務, 畢竟新注冊的任務的執行時間可能是要比前一陣拿到的隊首任務時間是要早的, 所以這里使用wait進行進行阻塞更合適, 那么喚醒操作就需要使用notify來實現了.
實現代碼如下:
//自己實現的定時器類 class MyTimer { //掃描線程 private Thread t = null; //阻塞隊列,存放任務 private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>(); public MyTimer() { //構造掃描線程 t = new Thread(() -> { while (true) { //取出隊首元素,檢查隊首元素執行任務的時間 //時間沒到,再把任務放回去 //時間到了,就執行任務 try { synchronized (this) { MyTask task = queue.take(); long curTime = System.currentTimeMillis(); if (curTime < task.getTime()) { //時間沒到,放回去 queue.put(task); //放回任務后,不應該立即就再次取出該任務 //所以wait設置一個阻塞等待,以便新任務到時間或者新任務來時后再取出來 this.wait(task.getTime() - curTime); } else { //時間到了,執行任務 task.run(); } } } catch (InterruptedException e) { throw new RuntimeException(e); } } }); t.start(); } /** * 注冊任務的方法 * @param runnable 任務內容 * @param after 表示在多少毫秒之后執行. 形如 1000 */ public void schedule (Runnable runnable, long after) { //獲取當前時間的時間戳再加上任務時間 MyTask task = new MyTask(runnable, System.currentTimeMillis() + after); queue.put(task); //每次當新任務加載到阻塞隊列時,需要中途喚醒線程,因為新進來的任務可能是最早需要執行的 synchronized (this) { this.notify(); } } }
要注意上面掃描線程中的synchronized并不能只要針對wait方法加鎖, 如果只針對wait加鎖的話, 考慮一個極端的情況, 假設的掃描線程剛執行完put方法, 這個線程就被cpu調度走了, 此時另有一個線程在隊列中插入了新任務, 然后notify喚醒了線程, 而剛剛并沒有執行wait阻塞, notify就沒有起到什么作用, 當cpu再調度到這個線程, 這樣的話如果新插入的任務要比原來隊首的任務時間更早, 那么這個新任務就被錯過了執行時間, 這些線程安全問題真是防不勝防啊, 所以我們需要保證這些操作的原子性, 也就是上面的代碼, 擴大鎖的范圍, 保證每次notify都是有效的.
那么最后基于上面的代碼, 我們來測試一下這個定時器:
public class TestDemo23 { public static void main(String[] args) { MyTimer timer = new MyTimer(); timer.schedule(new Runnable() { @Override public void run() { System.out.println("2s后執行的任務1"); } }, 2000); timer.schedule(new Runnable() { @Override public void run() { System.out.println("2s后執行的任務1"); } }, 1000); } }
執行結果:
感謝各位的閱讀,以上就是“Java多線程如何實現定時器”的內容了,經過本文的學習后,相信大家對Java多線程如何實現定時器這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。