溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java多線程如何實(shí)現(xiàn)定時(shí)器

發(fā)布時(shí)間:2023-01-30 09:21:00 來源:億速云 閱讀:151 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要講解了“Java多線程如何實(shí)現(xiàn)定時(shí)器”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“Java多線程如何實(shí)現(xiàn)定時(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)造方法解釋
    1public Timer()無(wú)參, 定時(shí)器關(guān)聯(lián)的線程為前臺(tái)線程, 線程名為默認(rèn)值
    2public Timer(boolean isDaemon)指定定時(shí)器中關(guān)聯(lián)的線程類型, true(后臺(tái)線程), false(前臺(tái)線程)
    3public Timer(String name)指定定時(shí)器關(guān)聯(lián)的線程名, 線程類型為前臺(tái)線程
    4public 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)方法解釋
    1public void schedule(TimerTask task, long delay)指定任務(wù), 延遲多久執(zhí)行該任務(wù)
    2public void schedule(TimerTask task, Date time)指定任務(wù), 指定任務(wù)的執(zhí)行時(shí)間
    3public void schedule(TimerTask task, long delay, long period)連續(xù)執(zhí)行指定任務(wù), 延遲時(shí)間, 連續(xù)執(zhí)行任務(wù)的時(shí)間間隔, 毫秒為單位
    4public void schedule(TimerTask task, Date firstTime, long period)連續(xù)執(zhí)行指定任務(wù), 第一次任務(wù)的執(zhí)行時(shí)間, 連續(xù)執(zhí)行任務(wù)的時(shí)間間隔
    5public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)與方法4作用相同
    6public void scheduleAtFixedRate(TimerTask task, long delay, long period)與方法3作用相同
    7public 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é)果:

    Java多線程如何實(shí)現(xiàn)定時(shí)器

    觀察執(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é)果:

    Java多線程如何實(shí)現(xiàn)定時(shí)器

    感謝各位的閱讀,以上就是“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)注!

    向AI問一下細(xì)節(jié)

    免責(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)容。

    AI