Java多線程案例之定時器詳解
一. 定時器概述
1. 什么是定時器
定時器是一種實際開發(fā)中非常常用的組件, 類似于一個 “鬧鐘”, 達(dá)到一個設(shè)定的時間之后, 就執(zhí)行某個指定好的代碼.
比如網(wǎng)絡(luò)通信中, 如果對方 500ms 內(nèi)沒有返回數(shù)據(jù), 則斷開連接嘗試重連.
比如一個 Map, 希望里面的某個 key 在 3s 之后過期(自動刪除).
類似于這樣的場景就需要用到定時器.
2. 標(biāo)準(zhǔn)庫中的定時器
標(biāo)準(zhǔn)庫中提供了一個 Timer 類, Timer 類的核心方法為schedule.
Timer類構(gòu)造時內(nèi)部會創(chuàng)建線程, 有下面的四個構(gòu)造方法, 可以指定線程名和是否將定時器內(nèi)部的線程指定為后臺線程(即守護線程), 如果不指定, 定時器對象內(nèi)部的線程默認(rèn)為前臺線程.
| 序號 | 構(gòu)造方法 | 解釋 |
|---|---|---|
| 1 | public Timer() | 無參, 定時器關(guān)聯(lián)的線程為前臺線程, 線程名為默認(rèn)值 |
| 2 | public Timer(boolean isDaemon) | 指定定時器中關(guān)聯(lián)的線程類型, true(后臺線程), false(前臺線程) |
| 3 | public Timer(String name) | 指定定時器關(guān)聯(lián)的線程名, 線程類型為前臺線程 |
| 4 | public Timer(String name, boolean isDaemon) | 指定定時器關(guān)聯(lián)的線程名和線程類型 |
schedule 方法是給Timer注冊一個任務(wù), 這個任務(wù)在指定時間后進行執(zhí)行, TimerTask類就是專門描述定時器任務(wù)的一個抽象類, 它實現(xiàn)了Runnable接口.
public abstract class TimerTask implements Runnable // jdk源碼
| 序號 | 方法 | 解釋 |
|---|---|---|
| 1 | public void schedule(TimerTask task, long delay) | 指定任務(wù), 延遲多久執(zhí)行該任務(wù) |
| 2 | public void schedule(TimerTask task, Date time) | 指定任務(wù), 指定任務(wù)的執(zhí)行時間 |
| 3 | public void schedule(TimerTask task, long delay, long period) | 連續(xù)執(zhí)行指定任務(wù), 延遲時間, 連續(xù)執(zhí)行任務(wù)的時間間隔, 毫秒為單位 |
| 4 | public void schedule(TimerTask task, Date firstTime, long period) | 連續(xù)執(zhí)行指定任務(wù), 第一次任務(wù)的執(zhí)行時間, 連續(xù)執(zhí)行任務(wù)的時間間隔 |
| 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ù)隊列中的全部任務(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é)束, 即進程并沒有結(jié)束, 這是因為上面的代碼定時器內(nèi)部是開啟了一個線程去執(zhí)行任務(wù)的, 雖然任務(wù)執(zhí)行完成了, 但是該線程并沒有銷毀; 這和自己定義一個線程執(zhí)行完成 run 方法后就自動銷毀是不一樣的, Timer 本質(zhì)上是相當(dāng)于線程池, 它緩存了一個工作線程, 一旦任務(wù)執(zhí)行完成, 該工作線程就處于空閑狀態(tài), 等待下一輪任務(wù).
二. 定時器的簡單實現(xiàn)
首先, 我們需要定義一個類, 用來描述一個定時器當(dāng)中的任務(wù), 類要成員要有一個Runnable, 再加上一個任務(wù)執(zhí)行的時間戳, 具體還包含如下內(nèi)容:
- 構(gòu)造方法, 用來指定任務(wù)和任務(wù)的延遲執(zhí)行時間.
- 兩個get方法, 分別用來給外部對象獲取該對象的任務(wù)和執(zhí)行時間.
- 實現(xiàn)Comparable接口, 指定比較方式, 用于判斷定時器任務(wù)的執(zhí)行順序, 每次需要執(zhí)行時間最早的任務(wù).
class MyTask implements Comparable<MyTask>{
//要執(zhí)行的任務(wù)
private Runnable runnable;
//任務(wù)的執(zhí)行時間
private long time;
public MyTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
}
//獲取當(dāng)前任務(wù)的執(zhí)行時間
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);
}
}
然后就需要實現(xiàn)定時器類了, 我們需要使用一個數(shù)據(jù)結(jié)構(gòu)來組織定時器中的任務(wù), 需要每次都能將時間最早的任務(wù)找到并執(zhí)行, 這個情況我們可以考慮用優(yōu)先級隊列(即小根堆)來實現(xiàn), 當(dāng)然我們還需要考慮線程安全的問題, 所以我們選用優(yōu)先級阻塞隊列 PriorityBlockingQueue 是最合適的, 特別要注意在自定義的任務(wù)類當(dāng)中要實現(xiàn)比較方式, 或者實現(xiàn)一下比較器也行.
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
我們自己實現(xiàn)的定時器類中要有一個注冊任務(wù)的方法, 用來將任務(wù)插入到優(yōu)先級阻塞隊列中;
還需要有一個線程用來執(zhí)行任務(wù), 這個線程是從優(yōu)先級阻塞隊列中取出隊首任務(wù)去執(zhí)行, 如果這個任務(wù)還沒有到執(zhí)行時間, 那么線程就需要把這個任務(wù)再放會隊列當(dāng)中, 然后線程就進入等待狀態(tài), 線程等待可以使用sleep和wait, 但這里有一個情況需要考慮, 當(dāng)有新任務(wù)插入到隊列中時, 我們需要喚醒線程重新去優(yōu)先級阻塞隊列拿隊首任務(wù), 畢竟新注冊的任務(wù)的執(zhí)行時間可能是要比前一陣拿到的隊首任務(wù)時間是要早的, 所以這里使用wait進行進行阻塞更合適, 那么喚醒操作就需要使用notify來實現(xiàn)了.
實現(xiàn)代碼如下:
//自己實現(xiàn)的定時器類
class MyTimer {
//掃描線程
private Thread t = null;
//阻塞隊列,存放任務(wù)
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public MyTimer() {
//構(gòu)造掃描線程
t = new Thread(() -> {
while (true) {
//取出隊首元素,檢查隊首元素執(zhí)行任務(wù)的時間
//時間沒到,再把任務(wù)放回去
//時間到了,就執(zhí)行任務(wù)
try {
synchronized (this) {
MyTask task = queue.take();
long curTime = System.currentTimeMillis();
if (curTime < task.getTime()) {
//時間沒到,放回去
queue.put(task);
//放回任務(wù)后,不應(yīng)該立即就再次取出該任務(wù)
//所以wait設(shè)置一個阻塞等待,以便新任務(wù)到時間或者新任務(wù)來時后再取出來
this.wait(task.getTime() - curTime);
} else {
//時間到了,執(zhí)行任務(wù)
task.run();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
/**
* 注冊任務(wù)的方法
* @param runnable 任務(wù)內(nèi)容
* @param after 表示在多少毫秒之后執(zhí)行. 形如 1000
*/
public void schedule (Runnable runnable, long after) {
//獲取當(dāng)前時間的時間戳再加上任務(wù)時間
MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
queue.put(task);
//每次當(dāng)新任務(wù)加載到阻塞隊列時,需要中途喚醒線程,因為新進來的任務(wù)可能是最早需要執(zhí)行的
synchronized (this) {
this.notify();
}
}
}
要注意上面掃描線程中的synchronized并不能只要針對wait方法加鎖, 如果只針對wait加鎖的話, 考慮一個極端的情況, 假設(shè)的掃描線程剛執(zhí)行完put方法, 這個線程就被cpu調(diào)度走了, 此時另有一個線程在隊列中插入了新任務(wù), 然后notify喚醒了線程, 而剛剛并沒有執(zhí)行wait阻塞, notify就沒有起到什么作用, 當(dāng)cpu再調(diào)度到這個線程, 這樣的話如果新插入的任務(wù)要比原來隊首的任務(wù)時間更早, 那么這個新任務(wù)就被錯過了執(zhí)行時間, 這些線程安全問題真是防不勝防啊, 所以我們需要保證這些操作的原子性, 也就是上面的代碼, 擴大鎖的范圍, 保證每次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后執(zhí)行的任務(wù)1");
}
}, 2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("2s后執(zhí)行的任務(wù)1");
}
}, 1000);
}
}
執(zhí)行結(jié)果:

到此這篇關(guān)于Java多線程案例之定時器詳解的文章就介紹到這了,更多相關(guān)Java定時器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot使用GridFS實現(xiàn)文件的上傳和下載方式
這篇文章主要介紹了Spring Boot使用GridFS實現(xiàn)文件的上傳和下載方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10
IDEA maven compile報錯OutOfMemoryError(內(nèi)存溢出)解決及jvm分析
遇到Maven編譯時報OutOfMemoryError錯誤通常因為默認(rèn)的堆內(nèi)存大小不足,本文就來介紹一下OutOfMemoryError(內(nèi)存溢出)解決,具有一定的參考價值,感興趣的可以了解一下2024-10-10

