??前言👀~
上一章我們介紹了阻塞隊列以及生產者消息模式,今天我們來講講定時器
定時器
標準庫中的定時器
schedule()方法
掃描線程
手動實現定時器
任務類
存儲任務的數據結構
定時器類
如果各位對文章的內容感興趣的話,請點點小贊,關注一手不迷路,講解的內容我會搭配我的理解用我自己的話去解釋如果有什么問題的話,歡迎各位評論糾正 🤞🤞🤞
個人主頁:N_0050-CSDN博客
相關專欄:java SE_N_0050的博客-CSDN博客??java數據結構_N_0050的博客-CSDN博客??java EE_N_0050的博客-CSDN博客
定時器
定時器是個非常常見的組件,尤其是在網絡進行通信的時候,類似發郵件,類似于一個 "鬧鐘",達到一個設定的時間之后, 就執行某個指定好的代碼
舉個例子,當客戶端給服務器發送請求后,服務器半天沒有響應,就像你發郵件一樣,發的時候會轉圈圈,成功了就會顯示發送成功或者什么提示信息,如果服務器沒有響應,你這邊可能就一直在那轉圈圈。我們也不知道是什么原因造成的,可能是請求沒發過去,可能是響應丟了,也可能是服務器出現了問題。所以對于客戶端來說,也可以說對用戶來說,肯定不能一直等啊那體驗多不好啊,所以設置一個等待時間(最大的期限),過了這個等待時間把電腦砸了,開個玩笑,過了這個最大期限,我們選擇重新發一遍,或者直接不發,或者重開這個程序等等方式。這里的最大期限我們可以使用定時器去實現
標準庫中的定時器
首先我們先使用一下定時器Timer類,再去講解,代碼如下
public class Test1 {public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("啟動成功");}}, 1000);System.out.println("原神啟動");}
}
輸出結果
schedule()方法
這個方法涉及兩個參數 第一個參數描述了任務要做什么這里使用匿名內部類去創建一個TimerTask實例,第二個參數就是時間就是要在多長時間(單位為毫秒)后去執行,這個時間是根據當前時間為準然后根據你設定的時間來執行任務的,比如說現在11:00:00你設置1秒后執行就是11:00:01執行任務。然后前面用匿名內部類創建出來的TimerTask實例實現了Runnable接口,然后我們重寫方法定義自己要執行的任務通過schedule方法,接著再由掃描線程去執行
掃描線程
當我們創建出這個timer對象后,這個線程也就被創建出來了,后續要執行任務,都是通過這個線程去執行的
來看剛才這段代碼以及輸出結果
public class Test1 {public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("啟動成功");}}, 1000);System.out.println("原神啟動");}
}
輸出結果,你會發現整個進程并沒有結束,主線程執行schedule方法的時候,是把這個任務丟給timer對象中的一個線程去處理的,這個線程可以叫"掃描線程",你設置的時間一到,就去掃描任務也就是執行你寫的任務。
解釋:為什么整個進程沒有結束?timer中的這個線程阻止了進程結束,它在等我們再給它安排任務,相當于服務員,你有什么吩咐它就執行,沒有任務就在那等并且timer里可以安排多個任務
手動實現定時器
根據上面標準庫可以得出以下要求:
1.和上面標準庫提供的timer類一樣,我們需要一個掃描線程,然后去執行任務
2.需要一個數據結構,把所有要執行的任務保存起來
3.需要使用一個類,通過一個類的對象去來描述執行的任務(任務內容和執行時間)
任務類
首先寫一個用來描述任務的類,包含任務內容和執行時間
在設置任務執行時間的時候,有兩種方式,一種是相對的時間,一種是絕對的時間(完整的時間戳),兩種都可以這里我們選擇絕對時間,因為相對時間要計算間隔后的時間然后進行比較,絕對時間獲取當前時間戳加上任務執行時間然后進行比較。
下面是任務類的實現,不只這一種,最后完整代碼有兩種
//用來描述任務的類 包含任務的內容和執行時間
class MyTimerTask {private Runnable runnable;private long time;public MyTimerTask(Runnable runnable, long delay) {this.runnable = runnable;this.time = System.currentTimeMillis() + delay;//使用絕對時機 時間戳+傳入的時間}public Runnable getRunnable() {return runnable;}public long getTime() {return time;}
}
存儲任務的數據結構
這里的數據結構我們采用優先級隊列去保存需要執行的任務,因為我們肯定要先執行時間最少的任務,然后優先級隊列也就是堆,最頂層的就是最小的,并且優先級隊列取出元素(也就是獲取時間最少的任務)時間復雜度都為O(1)
public PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
但是注意優先隊列要求放入的元素是可以比較的,也就是我們的任務之間可以進行比較,所以我們還需要實現自定義比較器,使用時間進行比較。除了優先級隊列中的元素需要能進行比較的,還有二叉搜索樹也就是TreeMap和TreeSet
public PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {@Overridepublic int compare(MyTimerTask o1, MyTimerTask o2) {return (int) (o1.getTime() - o2.getTime());}});
定時器類
我們的定時器和標準庫中的定時器一樣,我們需要一個掃描線程執行任務,還需要一個schedule方法,上面的優先級隊列也放在定時器中,下面是代碼實現,需要注意線程不安全問題,會出現這樣的可能就比如主線程在向隊列添加元素的時候,掃描線程也在對隊列進行判斷,導致加入了元素的時候這里正好進行判斷,然后為空進入阻塞狀態
class MyTimer {//優先級隊列存儲任務 優先級隊列的元素要能進行比較 所以要實現比較器 我們根據時間進行比較public PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {@Overridepublic int compare(MyTimerTask o1, MyTimerTask o2) {return (int) (o1.getTime() - o2.getTime());}});public MyTimer() {//創建出定時器對象的時候 啟動掃描線程thread.start();}//給用戶調用的方法 傳入要完成的任務以及時間public void schedule(Runnable runnable, long delay) {synchronized (lock) {//避免線程不安全問題 有任務了就喚醒線程進行工作if (delay < 0) {throw new IllegalArgumentException("輸入的時間有誤");} else {queue.offer(new MyTimerTask(runnable, delay));//調用這個方法的時候 創建任務然后放進隊列進行處理lock.notify();}}}public Object lock = new Object();public Thread thread = new Thread(() -> {synchronized (lock) {while (true) {//即使沒任務 也等我們給它分配任務while (queue.isEmpty()) {//隊列為空進入阻塞 使用while保險起見try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}long currentTime = System.currentTimeMillis();//記錄當前時間MyTimerTask task = queue.peek();//先看任務的時間 如果到了再pollif (currentTime >= task.getTime()) {queue.poll();task.getRunnable().run();//獲取到引用去執行用戶的任務} else {}}}});
}
還有一個地方需要進行優化比如就是你設置執行任務的時間在10點半,然后else那塊不寫代碼,它會一直到while循環開始判斷一路下路,一直到時間到去執行任務,這樣做消耗太多cpu資源,解決辦法,讓線程在這里休息,使用帶參數的wait方法,當前執行任務時間減去當前時間作為參數
public Thread thread = new Thread(() -> {synchronized (lock) {while (true) {//即使沒任務 也等我們給它分配任務while (queue.isEmpty()) {//隊列為空進入阻塞 使用while保險起見try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}long currentTime = System.currentTimeMillis();//記錄當前時間MyTimerTask task = queue.peek();//先看任務的時間 如果到了再pollif (currentTime >= task.getTime()) {queue.poll();task.getRunnable().run();//獲取到引用去執行用戶的任務} else {}}}});
兩種完整代碼
第一種任務類是沒有直接實現Runnable接口
//用來描述任務的類 包含任務的內容和執行時間
class MyTimerTask {private Runnable runnable;private long time;public MyTimerTask(Runnable runnable, long delay) {this.runnable = runnable;this.time = System.currentTimeMillis() + delay;//使用絕對時機 時間戳+傳入的時間}public Runnable getRunnable() {return runnable;}public long getTime() {return time;}
}//定時器
class MyTimer {//優先級隊列存儲任務 優先級隊列的元素要能進行比較 所以要實現比較器 我們根據時間進行比較public PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {@Overridepublic int compare(MyTimerTask o1, MyTimerTask o2) {return (int) (o1.getTime() - o2.getTime());}});public MyTimer() {//創建出定時器對象的時候 啟動掃描線程thread.start();}//給用戶調用的方法 傳入要完成的任務以及時間public void schedule(Runnable runnable, long delay) {synchronized (lock) {//避免線程不安全問題 有任務了就喚醒線程進行工作if (delay < 0) {throw new IllegalArgumentException("輸入的時間有誤");} else {queue.offer(new MyTimerTask(runnable, delay));//調用這個方法的時候 創建任務然后放進隊列進行處理lock.notify();}}}public Object lock = new Object();public Thread thread = new Thread(() -> {synchronized (lock) {while (true) {//即使沒任務 也等我們給它分配任務while (queue.isEmpty()) {//隊列為空進入阻塞 使用while保險起見try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}long currentTime = System.currentTimeMillis();//記錄當前時間MyTimerTask task = queue.peek();//先看任務的時間 如果到了再pollif (currentTime >= task.getTime()) {queue.poll();task.getRunnable().run();//獲取到引用去執行用戶的任務} else {}}}});
}
第二種是任務類實現Runnable接口
//用來描述任務的類 就是存儲任務的內容以及執行時間
class MyTimerTask implements Runnable {private long time;private Runnable task;public MyTimerTask(Runnable runnable, long delay) {this.task = runnable;this.time = System.currentTimeMillis() + delay;//使用絕對時間 當前時間戳+多少秒后執行=執行時間}public long getTime() {return time;}@Overridepublic void run() {//外層的這個就是一個殼,通過調用這個方法執行里面我們自己寫的任務task.run();// 這個就是我們自己寫的任務}
}//定時器 包含存儲隊列 掃描線程 創建任務
class MyTimer {public Object lock = new Object();//使用優先級隊列存儲任務 因為取出任務的時間復雜度為0(1) 注意要比較器 因為我們要使用時間比較出誰是最小的public PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {@Overridepublic int compare(MyTimerTask o1, MyTimerTask o2) {return (int) (o1.getTime() - o2.getTime());//return Long.compare(o1.getTime(), o2.getTime());//可以避免溢出}});//初始化定時器就啟動掃描線程public MyTimer() {thread.start();}//把任務和執行時間傳到這方法 然后通過這個方法創建任務類去裝任務和時間public void schedule(Runnable runnable, long delay) {synchronized (lock) {if (delay < 0) {throw new IllegalArgumentException("輸入的時間有誤!!!");} else {queue.offer(new MyTimerTask(runnable, delay));lock.notify();}}}//創建掃描線程執行任務public Thread thread = new Thread(() -> {//因為掃描線程會一直掃描任務 它在等我們再給它安排任務while (true) {synchronized (lock) {while (queue.isEmpty()) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}//不是直接poll 任務執行的時候要和當前時間進行比較后 再進行poll去執行//這個拿的任務相當于我們自己寫的任務MyTimerTask task = queue.peek();long currentTime = System.currentTimeMillis();//如果當前時間等于或者超過任務的執行時間就執行任務if (currentTime >= task.getTime()) {task.run();//queue.poll();} else {try {//讓線程休息到執行任務的時間lock.wait(task.getTime() - currentTime);} catch (InterruptedException e) {e.printStackTrace();}}}}});
}
以上便是本章內容,定時器在日常開發中還是會用到的,例如發郵件這類的,所以還是需要好好掌握,我們下一章再見💕