Java多線程案例之定時(shí)器詳解
一. 定時(shí)器概述
1. 什么是定時(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í)器.
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類就是專門描述定時(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ù).
二. 定時(shí)器的簡(jiǎn)單實(shí)現(xiàn)
首先, 我們需要定義一個(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é)果:
到此這篇關(guān)于Java多線程案例之定時(shí)器詳解的文章就介紹到這了,更多相關(guān)Java定時(shí)器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot @Configuration @bean注解作用解析
這篇文章主要介紹了springboot @Configuration @bean注解作用解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02redisson.tryLock()參數(shù)的使用及理解
這篇文章主要介紹了redisson.tryLock()參數(shù)的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-04-04Spring mvc如何實(shí)現(xiàn)數(shù)據(jù)處理
這篇文章主要介紹了Spring mvc如何實(shí)現(xiàn)數(shù)據(jù)處理,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03Java編程基于快速排序的三個(gè)算法題實(shí)例代碼
這篇文章主要介紹了Java編程基于快速排序的三個(gè)算法題實(shí)例代碼,小編覺得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01Java結(jié)構(gòu)型設(shè)計(jì)模式之適配器模式詳解
適配器模式,即將某個(gè)類的接口轉(zhuǎn)換成客戶端期望的另一個(gè)接口的表示,主要目的是實(shí)現(xiàn)兼容性,讓原本因?yàn)榻涌诓黄ヅ?,沒辦法一起工作的兩個(gè)類,可以協(xié)同工作。本文將通過示例詳細(xì)介紹適配器模式,需要的可以參考一下2022-09-09關(guān)于ArrayList的動(dòng)態(tài)擴(kuò)容機(jī)制解讀
這篇文章主要介紹了關(guān)于ArrayList的動(dòng)態(tài)擴(kuò)容機(jī)制解讀,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10Spring Boot處理全局統(tǒng)一異常的兩種方法與區(qū)別
這篇文章主要給大家介紹了關(guān)于Spring Boot處理全局統(tǒng)一異常的兩種方法與區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06