目錄
?編輯
定時器
定時器的使用?
三.定時器的實現 MyTimer
3.1 分析思路
1. 創建執行任務的類。
?2. 管理任務???
3. 執行任務
3.2 線程安全問題
?
定時器
定時器是軟件開發中的一個重要組件. 類似于一個 "鬧鐘". 達到一個設定的時間之后, 就執行某個指定好的代碼.
定時器的使用?
標準庫中的定時器 標準庫中提供了一個 Timer 類. Timer 類的核心方法為 schedule . schedule 包含兩個參數.
第一個參數是繼承timetask抽象類的類實例且內部重寫了run方法(這里的匿名類隱式繼承了TimerTask):指定即將要執行的任務代碼(timetask實現了runable接口所以有run方法)
?第二個參數指定多長時間之后執行 (單位為毫秒).
Timer timer = new Timer(); timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");} }, 3000);
執行schedule方法的時候,系統把要執行的任務放到timer對象中,與此同時timer對象里頭自帶一個線程叫做“掃描線程”,一旦時間到掃描線程就會執行剛才安排的任務,執行完所有任務后線程也不會銷毀,會阻塞等待直到其他的任務被放到timer對象中再繼續執行(就這么重復
三.定時器的實現 MyTimer
3.1 分析思路
對于定時器來說:
- 創建描述一個要執行的任務(任務內容 + 執行任務時間)的類
- 管理多個任務,通過一定的數據結構,把多個任務存起來
- 有專門的線程,執行這里的任務
1. 創建執行任務的類。
? ?我們在調用 schedule 時,傳的是延遲時間 “delay” 值。但是,描述任務時,不太建議使用 delay
表示,最好使用 “絕對時間”(時間戳)來表示~~
public class MyTimerTask implements Comparable<MyTimerTask>{//此處這里的 time,通過毫秒時間戳,表示這個任務具體啥時候執行private long time;private Runnable runnable;public MyTimerTask(Runnable runnable,long delay){this.time = System.currentTimeMillis() + delay;this.runnable = runnable;}public void run(){runnable.run();}public long getTime(){return time;}@Overridepublic int compareTo(MyTimerTask o) {//比如,當前時間是 10:30,任務時間是 12:00,不應該執行//如果當前時間是 10:30,任務時間是 10:29,應該執行//誰減去誰,可以通過實驗判斷return (int) (this.time - o.time);} }
?2. 管理任務???
使用 List 管理任務,不是一個好選擇——因為后續執行列表中的任務時,就需要依次遍歷每個元素;執行完畢后,還需要把對應的任務從 List 中刪除掉。???
我們需要按照時間來執行這里的任務。只要能夠確定所有任務中,時間最早的任務,判定它是否到該執行的時間即可。如果時間最早的任務還沒到執行時間,其他任務更不可能到時間了。因此,我們使用堆數據結構(涉及到隊列中的元素排序時,考慮堆)——PriorityQueue<MyTimerTask>(優先級隊列管理元素時,需要有比較方法,才能排序存儲。因此,在實現 MyTimerTask 類時,要繼承 Comparable<MyTimerTask> 接口,重寫 compareTo比較方法)
3. 執行任務
當創建 MyTimer 對象,調用無參構造方法時,便創建一個線程,循環執行從隊列中取出任務的操作:取出隊列中 “絕對時間” 最早的任務——如果當前時間 >= 此任務的時間(已經到達此任務的執行時間),便可調用run方法執行,執行完畢后從隊列中刪除; 如果當前時間 < 此任務的時間(沒到此任務的執行時間),則繼續執行循環。(所以,在實現 MyTimerTask 類時,要有 run方法 和 getTime方法)
? ?除此之外 ,還需要有 schedule 方法添加任務。
import java.util.PriorityQueue;public class MyTimer {private final PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();public MyTimer() {Thread t = new Thread(() -> {while (true){if(queue.isEmpty()){ continue;}MyTimerTask task = queue.peek();//判斷是否滿足執行條件if (System.currentTimeMillis() >= task.getTime()) {task.run();//執行完后,便從隊列中刪除queue.poll();}else{continue;}}});t.start();}public void schedule(Runnable runnable, long delay) {MyTimerTask task = new MyTimerTask(runnable, delay);queue.offer(task);}
}
3.2 線程安全問題
? ?當前這個代碼,是沒有考慮線程安全問題的。
? ?PriorityQueue 這個類自身,是非線程安全的,并且又是多個線程來進行操作,一定存在線程安全問題的風險。因此,要在涉及隊列相關操作的地方加鎖。(讓刪的時候不能進行加入操作,加的時候不進行刪除操作)
import java.util.PriorityQueue;public class MyTimer {private final PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();private final Object locker = new Object();public MyTimer() {Thread t = new Thread(() -> {while (true){synchronized(locker){if(queue.isEmpty()){ continue;}MyTimerTask task = queue.peek();if (System.currentTimeMillis() >= task.getTime()) {task.run();queue.poll();}else{continue;}}}});t.start();}public void schedule(Runnable runnable, long delay) {synchronized(locker){MyTimerTask task = new MyTimerTask(runnable, delay);queue.offer(task);}}
}
?但是,加完鎖以后,又出現了線程安全問題。?
1)初始情況下,如果隊列中,沒有任何元素。此時,就會在短時間內執行大量循環,這樣的執行是沒有意義的,導致“線程餓死”。
因此,我們需要添加 wait 和 notify 機制:隊列為空時,進行等待;添加任務時,就喚醒線程。
2)假設隊列中,已經包含元素了,并且當前時間是 10:45,任務時間 12:00(類似于,我定了12:00的鬧鐘,現在是 10:45)。在判斷任務是否滿足執行條件時,不滿足就會一直循環(相當于每隔一會兒就看一眼鬧鐘),這樣無意義的執行就一直占用著cpu資源,導致 “線程餓死”。
因此,我們需要添加一個有等待期限的 wait(等待 1h15min 就會執行),當到達任務執行時間,wait 就結束了。如果在等待過程中,又再次調用 schedule 方法,也會喚醒這里的 wait,進行新一輪的判斷。
?線程安全版:
import java.util.PriorityQueue;public class MyTimer {private final PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();private final Object locker = new Object();public MyTimer() {Thread t = new Thread(() -> {try {while (true) {synchronized (locker) {while (queue.isEmpty()) {//如果還沒添加任務,會不斷循環執行判斷,出現線程餓死。//continue;//因此,使用wait等待,當添加任務后喚醒locker.wait();}MyTimerTask task = queue.peek();if (System.currentTimeMillis() >= task.getTime()) {task.run();queue.poll();} else {//如果還沒到任務執行時間,依舊不斷循環判斷,出現線程餓死。//continue;//因此,使用有等待期限的 wait,計算執行的時間與當前時間的差值//當添加新的任務后,wait 被喚醒,再進行新的判斷locker.wait(task.getTime() - System.currentTimeMillis());}}}} catch (InterruptedException e) {e.printStackTrace();}});t.start();}public void schedule(Runnable runnable, long delay) {synchronized (locker){MyTimerTask task = new MyTimerTask(runnable, delay);queue.offer(task);// 喚醒 waitlocker.notify();}}
}
* 能否將第二處的 wait 改為 sleep 呢? ——不能!!
?不應該使用sleep,可能存在以下情況:
?????1)在 sleep 阻塞1h15min 的過程中,新來了一個時間更早的任務,比如 11:30 要執行。如果使用 wait ,每次新來的任務,都會把 wait 喚醒,重新設定 wait 的等待時間。而 sleep 不會被喚醒,依舊在阻塞著.....
?????2)sleep 休眠的時候,不會釋放鎖。因此,在休眠的時候就是“抱著鎖”,其他人想拿鎖就拿不到了。也就是說你休眠的時候,就不能進行增加線程
?* PriorityQueue 是線程不安全的類,能否使用 PriorityBlockingQueue 線程安全的阻塞隊列呢?——不能!!????
如果使用線程安全的隊列,會導致代碼中從 一把鎖 變成 兩把鎖,很容易出現死鎖的情況,比如持有和請求的情況(并非100%一定出現,但是需要程序員精心控制加鎖順序,使得編寫代碼的復雜度提高了。如果通篇代碼 只有一把鎖,就能更容易地解決問題)
?