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