1. 程式人生 > >Java多執行緒18:定時器Timer

Java多執行緒18:定時器Timer

前言

         定時/計劃功能在Java應用的各個領域都使用得非常多,比方說Web層面,可能一個專案要定時採集話單、定時更新某些快取、定時清理一批不活躍使用者等等。定時計劃任務功能在Java中主要使用的就是Timer物件,它在內部使用多執行緒方式進行處理,所以它和多執行緒技術關聯還是相當大的。那和ThreadLocal一樣,還是先講原理再講使用,Timer的實現原理不難,就簡單掃一下就好了。

1.Timer的schedule(TimeTask task, Date time)的使用

該方法的作用是在執行的日期執行一次任務

private static Timer timer = new Timer();
    
static public class MyTask extends TimerTask
{
    public void run()
    {
        System.out.println("運行了!時間為:" + new Date());
    }
}
    
public static void main(String[] args) throws Exception
{
    MyTask task = new MyTask();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String dateString = "2015-10-6 12:14:00";
    Date dateRef = sdf.parse(dateString);
    System.out.println("字串時間:" + dateRef.toLocaleString() + " 當前時間:" + new Date().toLocaleString());
    timer.schedule(task, dateRef);
}

看一下執行效果:

字串時間:2015-10-6 12:14:00 當前時間:2015-10-6 12:13:23
運行了!時間為:Tue Oct 06 12:14:00 CST 2015

執行時間和但前時間不一致,而是和dateRef的時間一直,證明了未來執行。任務雖然執行完了,但程序沒有銷燬,控制檯上的方框可以看到還是紅色的,看下Timer的原始碼:

public Timer() {
    this("Timer-" + serialNumber());
}
public Timer(String name) {
    thread.setName(name);
    thread.start();
}

     所以,啟動一個Timer就是啟動一個新執行緒,但是這個新執行緒並不是守護執行緒,所以它會一直執行。要執行完就讓程序停止的話,設定Timer為守護執行緒就好了,有專門的建構函式可以設定:

public Timer(boolean isDaemon) {
    this("Timer-" + serialNumber(), isDaemon);
}
public Timer(String name, boolean isDaemon) {
    thread.setName(name);
    thread.setDaemon(isDaemon);
    thread.start();
}

2、計劃時間早於當前時間:立即執行

如果執行任務的時間早於當前時間,那麼立即執行task的任務:

private static Timer timer = new Timer();
    
static public class MyTask extends TimerTask
{
    public void run()
    {
        System.out.println("運行了!時間為:" + new Date());
    }
}
    
public static void main(String[] args) throws Exception
{
    MyTask task = new MyTask();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String dateString = "2014-10-6 12:14:00";
    Date dateRef = sdf.parse(dateString);
    System.out.println("字串時間:" + dateRef.toLocaleString() + " 當前時間:" + new Date().toLocaleString());
    timer.schedule(task, dateRef);
}
字串時間:2014-10-6 12:14:00 當前時間:2015-10-6 12:20:10
運行了!時間為:Tue Oct 06 12:20:10 CST 2015

執行時間和當前時間一致,證明了立即執行

3、多個TimerTask任務執行

Timer中允許有多個任務:

private static Timer timer = new Timer();
    
static public class MyTask extends TimerTask
{
    public void run()
    {
        System.out.println("運行了!時間為:" + new Date());
    }
}
    
public static void main(String[] args) throws Exception
{
    MyTask task1 = new MyTask();
    MyTask task2 = new MyTask();
    SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String dateString1 = "2015-10-6 12:26:00";
    String dateString2 = "2015-10-6 12:27:00";
    Date dateRef1 = sdf1.parse(dateString1);
    Date dateRef2 = sdf2.parse(dateString2);
    System.out.println("字串時間:" + dateRef1.toLocaleString() + " 當前時間:" + new Date().toLocaleString());
    System.out.println("字串時間:" + dateRef2.toLocaleString() + " 當前時間:" + new Date().toLocaleString());
    timer.schedule(task1, dateRef1);
    timer.schedule(task2, dateRef2);
}

看一下執行結果:

字串時間:2015-10-6 12:26:00 當前時間:2015-10-6 12:25:38
字串時間:2015-10-6 12:27:00 當前時間:2015-10-6 12:25:38
運行了!時間為:Tue Oct 06 12:26:00 CST 2015
運行了!時間為:Tue Oct 06 12:27:00 CST 2015

    可以看到,執行時間和設定的時間一致,證明了未來可以執行多個任務。另外注意,Task是以佇列的方式一個一個被順序執行的,所以執行的時間有可能和預期的時間不一致,因為前面的任務可能消耗過長,後面任務的執行時間也有可能被延遲

      程式碼就不寫了,舉個例子,任務1計劃12:00:00被執行,任務2計劃12:00:10被執行,結果任務1執行了30秒,那麼任務2將在12:00:30被執行,因為Task是被放入佇列中的,因此必須一個一個順序執行。

Timer的schedule(TimerTask task, Date firstTime, long period)

     該方法的作用是在指定的日期之後,按指定的間隔週期性地無限迴圈地執行某一任務

  1. 計劃時間晚於當前時間:未來執行
static public class MyTask extends TimerTask
{
    public void run()
    {
        System.out.println("運行了!時間為:" + new Date());
    }
}
    
public static void main(String[] args) throws Exception
{
    MyTask task = new MyTask();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String dateString = "2015-10-6 18:00:00";
    Timer timer = new Timer();
    Date dateRef = sdf.parse(dateString);
    System.out.println("字串時間:" + dateRef.toLocaleString() + " 當前時間:" + new Date().toLocaleString());
    timer.schedule(task, dateRef, 4000);
}

看一下執行結果:

字串時間:2015-10-6 18:01:00 當前時間:2015-10-6 18:00:15
運行了!時間為:Tue Oct 06 18:01:00 CST 2015
運行了!時間為:Tue Oct 06 18:01:04 CST 2015
運行了!時間為:Tue Oct 06 18:01:08 CST 2015
運行了!時間為:Tue Oct 06 18:01:12 CST 2015
...

看到從設定的時間開始,每隔4秒列印一次,無限列印下去

2、計劃時間早於當前時間:立即執行

static public class MyTask extends TimerTask
{
    public void run()
    {
        System.out.println("運行了!時間為:" + new Date());
    }
}
    
public static void main(String[] args) throws Exception
{
    MyTask task = new MyTask();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String dateString = "2014-10-6 18:01:00";
    Timer timer = new Timer();
    Date dateRef = sdf.parse(dateString);
    System.out.println("字串時間:" + dateRef.toLocaleString() + " 當前時間:" + new Date().toLocaleString());
    timer.schedule(task, dateRef, 4000);
}

看一下執行結果:

字串時間:2014-10-6 18:01:00 當前時間:2015-10-6 18:02:46
運行了!時間為:Tue Oct 06 18:02:46 CST 2015
運行了!時間為:Tue Oct 06 18:02:50 CST 2015
運行了!時間為:Tue Oct 06 18:02:54 CST 2015
運行了!時間為:Tue Oct 06 18:02:58 CST 2015
運行了!時間為:Tue Oct 06 18:03:02 CST 2015
...

看到執行時間比當前時間早,從當前時間開始,每隔4秒列印一次,無限迴圈下去

TimerTask的cancel()方法

static public class MyTaskA extends TimerTask
{
    public void run()
    {
        System.out.println("A運行了!時間為:" + new Date());
        this.cancel();
    }
}
    
static public class MyTaskB extends TimerTask
{
    public void run()
    {
        System.out.println("B運行了!時間為:" + new Date());
    }
}
    
public static void main(String[] args) throws Exception
{
    MyTaskA taskA = new MyTaskA();
    MyTaskB taskB = new MyTaskB();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String dateString = "2015-10-6 18:10:00";
    Timer timer = new Timer();
    Date dateRef = sdf.parse(dateString);
    System.out.println("字串時間:" + dateRef.toLocaleString() + " 當前時間:" + new Date().toLocaleString());
    timer.schedule(taskA, dateRef, 4000);
    timer.schedule(taskB, dateRef, 4000);    
}

看一下執行結果:

字串時間:2015-10-6 18:10:00 當前時間:2015-10-6 18:09:47
A運行了!時間為:Tue Oct 06 18:10:00 CST 2015
B運行了!時間為:Tue Oct 06 18:10:00 CST 2015
B運行了!時間為:Tue Oct 06 18:10:04 CST 2015
B運行了!時間為:Tue Oct 06 18:10:08 CST 2015
B運行了!時間為:Tue Oct 06 18:10:12 CST 2015
...

看到TimeTask的cancel()方法是將自身從任務佇列中被移除,其他任務不受影響

Timer的cancel()方法

把上面程式碼改動一下:

private static Timer timer = new Timer();
    
static public class MyTaskA extends TimerTask
{
    public void run()
    {
        System.out.println("A運行了!時間為:" + new Date());
        timer.cancel();
    }
}
    
static public class MyTaskB extends TimerTask
{
    public void run()
    {
        System.out.println("B運行了!時間為:" + new Date());
    }
}
    
public static void main(String[] args) throws Exception
{
    MyTaskA taskA = new MyTaskA();
    MyTaskB taskB = new MyTaskB();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String dateString = "2015-10-6 18:10:00";
    Date dateRef = sdf.parse(dateString);
    System.out.println("字串時間:" + dateRef.toLocaleString() + " 當前時間:" + new Date().toLocaleString());
    timer.schedule(taskA, dateRef, 4000);
    timer.schedule(taskB, dateRef, 4000);
}

看一下執行結果:

字串時間:2015-10-6 18:10:00 當前時間:2015-10-6 18:14:15
A運行了!時間為:Tue Oct 06 18:14:15 CST 2015

       全部任務都被清除,並且程序被銷燬。不過注意一下,cancel()方法未必一定會停止執行計劃任務,可能正常執行,因為cancel()方法會嘗試去獲取queue鎖,如果並沒有獲取到queue鎖的話,TimerTask類中的任務繼續執行也是完全有可能的

其他方法

再列舉一些Timer中的其他schedule的過載方法的作用,就不提供證明的程式碼了,可以自己嘗試一下:

1、schedule(TimerTask task, long delay)

以當前時間為參考,在此時間基礎上延遲指定的毫秒數後執行一次TimerTask任務

2、schedule(TimerTask task, long delay, long period)

以當前時間為參考,在此時間基礎上延遲指定的毫秒數後,以period為迴圈週期,迴圈執行TimerTask任務

3、scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

在延時的場景下,schedule方法和scheduleAtFixedRate方法沒有區別,它們的區別只是在非延時上。如果執行任務的時間沒有被延時,對於schedule方法來說,下一次任務執行的時間參考的是上一次任務的開始時間來計算的;對於scheduleAtFixedRate方法來說,下一次任務執行的時間參考的是上一次任務的結束時間來計算的