天天看點

Java中的定時器:java.util.Timer

作者:IT技術控

1.定時器

1.1 含義

  在Java中,定時器(Timer)是一個工具類,用于安排任務(Task)在指定時間後執行或以指定的時間間隔重複執行。它可以用于執行定時任務、定時排程和時間延遲等操作。

定時器(Timer)可以應用于許多場景,比如:

  1. 排程任務:當你需要按照預定時間執行任務時,可以使用定時器。例如,每天淩晨執行資料備份、定時生成報表、定時發送通知等。
  2. 逾時處理:當你需要處理某個操作的逾時情況時,可以使用定時器。例如,設定一個操作的逾時時間,如果在規定時間内未完成,則執行相應的逾時處理邏輯。

1.2 标準庫中的定時器

  Java中的定時器:java.util.Timer,它的常用方法:

方法 描述
schedule(TimerTask task, Date time) 安排在指定時間執行任務。
schedule(TimerTask task, long delay) 安排在指定延遲時間後執行任務。
schedule(TimerTask task, long delay, long period) 安排在指定延遲時間後以指定的時間間隔重複執行任務。
scheduleAtFixedRate(TimerTask task, Date firstTime, long period) 安排在指定時間開始以固定的時間間隔重複執行任務。
scheduleAtFixedRate(TimerTask task, long delay, long period) 安排在指定延遲時間後以固定的時間間隔重複執行任務。
cancel() 取消定時器的所有任務。
purge() 從定時器的任務隊列中删除所有已取消的任務。
java複制代碼public class Main {
    public static void main(String[] args) {
        
        Timer timer = new Timer();
        //排程指定的任務在指定的延遲時間(3000ms)後執行。
        timer.schedule(new TimerTask() {
            //待執行的任務
            @Override
            public void run() {
                System.out.println("hello");
            }
        },3000);
        
    }
}
           

也可以一次注冊多個任務:

java複制代碼public class Main {
    public static void main(String[] args) {

        Timer timer = new Timer();
        //在指定的延遲時間(1000ms)後執行。
        timer.schedule(new TimerTask() {
            //待執行的任務
            @Override
            public void run() {
                System.out.println("任務1");
            }
        },1000);

        //在指定的延遲時間(2000ms)後執行。
        timer.schedule(new TimerTask() {
            //待執行的任務
            @Override
            public void run() {
                System.out.println("任務2");
            }
        },2000);

        //在指定的延遲時間(3000ms)後執行。
        timer.schedule(new TimerTask() {
            //待執行的任務
            @Override
            public void run() {
                System.out.println("任務3");
            }
        },3000);
    }
}
           

2.簡單模拟實作定時器

2.1 實作思路

  1. 使用一個資料結構來儲存所有的任務,這些任務是根據時間的大小來進行先後執行的,是以這裡使用優先級隊列。由于這裡是多線程的環境,是以這裡采用PriorityBlockingQueue(優先級阻塞隊列),時間越小優先級越高。
  2. 我們需要使用一個線程來掃描定時器裡面的任務是否到達執行時間,由于我們采用的是優先級隊列資料結構,是以隻需掃描隊首元素。如果隊首還沒到執行時間,那麼後面的元素不可能到達執行時間。
  3. 任務用一個類MyTask來表示,這裡需要實作Comparable接口,因為它需要存入優先級隊列。其中的屬性:
java複制代碼//表示定時器中的任務
class MyTask implements Comparable<MyTask>{
    //要執行的任務内容
    private Runnable runnable;

    //延遲時間
    private long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }


    //為了便于後面的比較,需要提供 get 方法
    public long getTime() {
        return time;
    }

    //表示任務開始執行
    public void run(){
        this.runnable.run();
    }
    
    @Override
    public int compareTo(MyTask o) {
        return (int)(this.getTime() - o.getTime());
    }
}
           
  1. 實作添加任務的方法schedule:
java複制代碼public class MyTimer {

    //掃描線程
    private Thread thread;

    //優先級隊列(這裡為阻塞隊列)
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    /**
     * 這個方法是用來注冊(添加)任務的
     * @param runnable 表示待執行的任務
     * @param after 表示多少時間過後執行任務
     */
    public void schedule(Runnable runnable,long after){
        //添加任務,注意這裡的時間是 System.currentTimeMillis() + after
        MyTask task = new MyTask(runnable,System.currentTimeMillis() + after);
        queue.put(task);
    }
}
           
  1. 添加一個線程來檢測隊首元素:
java複制代碼    //當建立對象的時候就直接開啟一個線程
	public MyTimer(){
        thread = new Thread(()->{
           while(true){
               //取出隊首,如果到時間了就執行。
               try {
                   MyTask myTask = queue.take();
                   long curTime = System.currentTimeMillis();
                   if(curTime < myTask.getTime()){
                       //時間未到,不執行
                       queue.put(myTask);
                   }else {
                       //時間已到,執行
                       myTask.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();
    }
           

  就這樣就完了嗎?其實不然,在上面代碼中while (true)轉的太快了, 造成了無意義的 CPU 浪費,如果第一個任務設定的是 1 min 之後執行某個邏輯,那麼在這一分鐘内 CPU 會一直存取隊首元素。是以這裡需要借助該對象的wait / notify來解決 while (true) 的忙等問題。

java複制代碼    public MyTimer(){
        thread = new Thread(()->{
           while(true){
               //取出隊首,如果到時間了就執行。
               try {
                   MyTask myTask = queue.take();
                   long curTime = System.currentTimeMillis();
                   if(curTime < myTask.getTime()){
                       queue.put(myTask);
                       //時間未到,不執行,這裡的 this 表示 MyTimer 對象
                       synchronized (this){
                           //阻塞一段時間
                           this.wait(myTask.getTime() - curTime);
                       }
                   }else {
                       //時間已到,執行
                       myTask.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();
    }


    /**
     * 這個方法是用來注冊(添加)任務的
     * @param runnable 表示待執行的任務
     * @param after 表示多少時間過後執行任務
     */
    public void schedule(Runnable runnable,long after){
        //添加任務,注意這裡的時間是 System.currentTimeMillis() + after
        MyTask task = new MyTask(runnable,System.currentTimeMillis() + after);
        queue.put(task);
        synchronized(this){
            this.notify();
        }
    }
           

  修改 Timer 的 schedule 方法,每次有新任務到來的時候喚醒一下線程。(因為新插入的任務可能是需要馬上執行的)。

  還沒結束!上面的代碼還是有缺陷的。假設當 thread 線程執行完 queue.take() 過後,myTask.getTime() - curTime 的值為 1 個小時。這時 CPU 排程了其它線程(假設為 t2) 執行, t2 線程調用 schedule 方法,延時時間為 30 分鐘,并調用 put 方法,随後再執行 notify 方法。然而這時 wait 方法還沒有執行,notify 相當于失效了。這時CPU再排程 thread 線程執行,但是 myTask.getTime() - curTime 的值本應是 30 分鐘(新添加了一個任務),但是實際上卻是 1 個小時。   這是因為queue.take()與wait不是原子操作,是以才導緻這個問題的發生,下面是改進後的代碼。

java複制代碼    public MyTimer(){
        thread = new Thread(()->{
           while(true){
               //取出隊首,如果到時間了就執行。
               try {
                   synchronized (this){
                       MyTask myTask = queue.take();
                       long curTime = System.currentTimeMillis();
                       if(curTime < myTask.getTime()){
                           queue.put(myTask);
                           //時間未到,不執行
                           //阻塞一段時間
                           this.wait(myTask.getTime() - curTime);
                       }else {
                           //時間已到,執行
                           myTask.run();
                       }                       
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();
    }
           

2.2 完整代碼

java複制代碼//表示定時器中的任務
class MyTask implements Comparable<MyTask>{
    //要執行的任務内容
    private Runnable runnable;

    //延遲時間
    private long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }


    //為了便于後面的比較,需要提供 get 方法
    public long getTime() {
        return time;
    }

    //表示任務開始執行
    public void run(){
        this.runnable.run();
    }

    @Override
    public int compareTo(MyTask o) {
        return (int)(this.getTime() - o.getTime());
    }
}

public class MyTimer {

    //掃描線程
    private Thread thread;

    //優先級隊列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public MyTimer(){
        thread = new Thread(()->{
           while(true){
               //取出隊首,如果到時間了就執行。
               try {
                   synchronized (this){
                       MyTask myTask = queue.take();
                       long curTime = System.currentTimeMillis();
                       if(curTime < myTask.getTime()){
                           queue.put(myTask);
                           //時間未到,不執行
                           //阻塞一段時間
                           this.wait(myTask.getTime() - curTime);
                       }else {
                           //時間已到,執行
                           myTask.run();
                       }
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();
    }


    /**
     * 這個方法是用來注冊(添加)任務的
     * @param runnable 表示待執行的任務
     * @param after 表示多少時間過後執行任務
     */
    public void schedule(Runnable runnable,long after){
        //添加任務,注意這裡的時間是 System.currentTimeMillis() + after
        MyTask task = new MyTask(runnable,System.currentTimeMillis() + after);
        queue.put(task);
        synchronized(this){
            this.notify();
        }
    }
}