一. 定時(shí)器概述
1. 什么是定時(shí)器
定時(shí)器是一種實(shí)際開(kāi)發(fā)中非常常用的組件, 類似于一個(gè) “鬧鐘”, 達(dá)到一個(gè)設(shè)定的時(shí)間之后, 就執(zhí)行某個(gè)指定好的代碼.
- 比如網(wǎng)絡(luò)通信中, 如果對(duì)方 500ms 內(nèi)沒(méi)有返回?cái)?shù)據(jù), 則斷開(kāi)連接嘗試重連.
- 比如一個(gè) Map, 希望里面的某個(gè) key 在 3s 之后過(guò)期(自動(dòng)刪除).
類似于這樣的場(chǎng)景就需要用到定時(shí)器.
2. 標(biāo)準(zhǔn)庫(kù)中的定時(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
類就是專門(mén)描述定時(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é)束后程序并沒(méi)有結(jié)束, 即進(jìn)程并沒(méi)有結(jié)束, 這是因?yàn)樯厦娴拇a定時(shí)器內(nèi)部是開(kāi)啟了一個(gè)線程去執(zhí)行任務(wù)的, 雖然任務(wù)執(zhí)行完成了, 但是該線程并沒(méi)有銷毀; 這和自己定義一個(gè)線程執(zhí)行完成 run 方法后就自動(dòng)銷毀是不一樣的, Timer 本質(zhì)上是相當(dāng)于線程池, 它緩存了一個(gè)工作線程, 一旦任務(wù)執(zhí)行完成, 該工作線程就處于空閑狀態(tài), 等待下一輪任務(wù).
二. 定時(shí)器的簡(jiǎn)單實(shí)現(xiàn)
首先, 我們需要定義一個(gè)類, 用來(lái)描述一個(gè)定時(shí)器當(dāng)中的任務(wù), 類要成員要有一個(gè)Runnable, 再加上一個(gè)任務(wù)執(zhí)行的時(shí)間戳, 具體還包含如下內(nèi)容:
- 構(gòu)造方法, 用來(lái)指定任務(wù)和任務(wù)的延遲執(zhí)行時(shí)間.
- 兩個(gè)get方法, 分別用來(lái)給外部對(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)來(lái)組織定時(shí)器中的任務(wù), 需要每次都能將時(shí)間最早的任務(wù)找到并執(zhí)行, 這個(gè)情況我們可以考慮用優(yōu)先級(jí)隊(duì)列(即小根堆)來(lái)實(shí)現(xiàn), 當(dāng)然我們還需要考慮線程安全的問(wèn)題, 所以我們選用優(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ù)的方法, 用來(lái)將任務(wù)插入到優(yōu)先級(jí)阻塞隊(duì)列中;
還需要有一個(gè)線程用來(lái)執(zhí)行任務(wù), 這個(gè)線程是從優(yōu)先級(jí)阻塞隊(duì)列中取出隊(duì)首任務(wù)去執(zhí)行, 如果這個(gè)任務(wù)還沒(méi)有到執(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
來(lái)實(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í)間沒(méi)到,再把任務(wù)放回去
//時(shí)間到了,就執(zhí)行任務(wù)
try {
synchronized (this) {
MyTask task = queue.take();
long curTime = System.currentTimeMillis();
if (curTime < task.getTime()) {
//時(shí)間沒(méi)到,放回去
queue.put(task);
//放回任務(wù)后,不應(yīng)該立即就再次取出該任務(wù)
//所以wait設(shè)置一個(gè)阻塞等待,以便新任務(wù)到時(shí)間或者新任務(wù)來(lái)時(shí)后再取出來(lái)
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)來(lái)的任務(wù)可能是最早需要執(zhí)行的
synchronized (this) {
this.notify();
}
}
}
要注意上面掃描線程中的synchronized
并不能只要針對(duì)wait
方法加鎖, 如果只針對(duì)wait
加鎖的話, 考慮一個(gè)極端的情況, 假設(shè)的掃描線程剛執(zhí)行完put方法, 這個(gè)線程就被cpu調(diào)度走了, 此時(shí)另有一個(gè)線程在隊(duì)列中插入了新任務(wù), 然后notify
喚醒了線程, 而剛剛并沒(méi)有執(zhí)行wait
阻塞, notify
就沒(méi)有起到什么作用, 當(dāng)cpu再調(diào)度到這個(gè)線程, 這樣的話如果新插入的任務(wù)要比原來(lái)隊(duì)首的任務(wù)時(shí)間更早, 那么這個(gè)新任務(wù)就被錯(cuò)過(guò)了執(zhí)行時(shí)間, 這些線程安全問(wèn)題真是防不勝防啊, 所以我們需要保證這些操作的原子性, 也就是上面的代碼, 擴(kuò)大鎖的范圍, 保證每次notify都是有效的.
那么最后基于上面的代碼, 我們來(lái)測(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é)果:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-829902.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-829902.html
到了這里,關(guān)于Java多線程案例之定時(shí)器的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!