深刻探究JDK中Timer的使用方式与源码解析

导言

在项目开发过程当中,常常会遇到须要使用定时执行或延时执行任务的场景。好比咱们在活动结束后自动汇总生成效果数据、导出Excel表并将文件经过邮件推送到用户手上,再好比微信运动天天都会在十点后向你发送个位数的步数(在?把摄像头从我家拆掉!)。html

本文将会介绍java.util.Timer的使用,并从源码层面对它进行解析。java

定时器Timer的使用

java.util.Timer是JDK提供的很是使用的工具类,用于计划在特定时间后执行的任务,能够只执行一次或按期重复执行。在JDK内部不少组件都是使用的java.util.Timer实现定时任务或延迟任务。api

Timer能够建立多个对象的实例,每一个对象都有且只有一个后台线程来执行任务。数组

Timer类是线程安全的,多个线程能够共享一个计时器,而无需使用任何的同步。

构造方法

首先咱们能够看下Timer类的构造方法的API文档安全

 

  1. Timer(): 建立一个新的计时器。
  2. Timer(boolean isDaemon): 建立一个新的定时器,其关联的工做线程能够指定为守护线程。
  3. Timer(String name): 建立一个新的定时器,其关联的工做线程具备指定的名称。
  4. Timer(String name, boolean isDaemon): 建立一个新的定时器,其相关线程具备指定的名称,能够指定为守护线程。

Note: 守护线程是低优先级线程,在后台执行次要任务,好比垃圾回收。当有非守护线程在运行时,Java应用不会退出。若是全部的非守护线程都退出了,那么全部的守护线程也会随之退出。微信

实例方法

接下来咱们看下Timer类的实例方法的API文档数据结构

  1. cancel(): 终止此计时器,并丢弃全部当前执行的任务。
  2. purge(): 从该计时器的任务队列中删除全部取消的任务。
  3. schedule(TimerTask task, Date time): 在指定的时间执行指定的任务。
  4. schedule(TimerTask task, Date firstTime, long period): 从指定 的时间开始 ,对指定的任务按照固定的延迟时间重复执行 。
  5. schedule(TimerTask task, long delay): 在指定的延迟以后执行指定的任务。
  6. schedule(TimerTask task, long delay, long period): 在指定的延迟以后开始 ,对指定的任务按照固定的延迟时间重复执行 。
  7. scheduleAtFixedRate(TimerTask task, Date firstTime, long period): 从指定的时间开始 ,对指定的任务按照固定速率重复执行 。
  8. scheduleAtFixedRate(TimerTask task, long delay, long period): 在指定的延迟以后开始 ,对指定的任务按照固定速率重复执行。

schedulescheduleAtFixedRate都是重复执行任务,区别在于schedule是在任务成功执行后,再按照固定周期再从新执行任务,好比第一次任务从0s开始执行,执行5s,周期是10s,那么下一次执行时间是15s而不是10s。而scheduleAtFixedRate是从任务开始执行时,按照固定的时间再从新执行任务,好比第一次任务从0s开始执行,执行5s,周期是10s,那么下一次执行时间是10s而不是15s。oracle

使用方式

1. 执行时间晚于当前时间

接下来咱们将分别使用schedule(TimerTask task, Date time)schedule(TimerTask task, long delay)用来在10秒后执行任务,并展现是否将Timer的工做线程设置成守护线程对Timer执行的影响。ide

首先咱们建立类Task, 接下来咱们的全部操做都会在这个类中执行, 在类中使用schedule(TimerTask task, Date time),代码以下函数

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序结束时间为: {0}", currentTimeMillis()));
        }));

        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序执行时间为: {0}", startTimestamp));
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp + 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任务运行在线程[{0}]上, 指望执行时间为[{1}], 实际执行时间为[{2}], 实际误差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, new Date(startTimestamp + 10 * SECOND));
    }
}

在程序的最开始,咱们注册程序结束时执行的函数,它用来打印程序的结束时间,咱们稍后将会用它来展现工做线程设置为守护线程与非守护线程的差别。接下来是程序的主体部分,咱们记录了程序的执行时间,定时任务执行时所在的线程、定时任务的指望执行时间与实际执行时间。

程序运行后的实际执行效果

程序执行时间为: 1,614,575,921,461
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,575,931,461], 实际执行时间为[1,614,575,931,464], 实际误差[3]

程序在定时任务执行结束后并无退出,咱们注册的生命周期函数也没有执行,咱们将在稍后解释这个现象。

接下来咱们在类中使用schedule(TimerTask task, long delay), 来达到相同的在10秒钟以后执行的效果

import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序结束时间为: {0}", currentTimeMillis()));
        }));

        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序执行时间为: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp + 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任务运行在线程[{0}]上, 指望执行时间为[{1}], 实际执行时间为[{2}], 实际误差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND);
    }
}

程序运行后的实际执行效果

程序执行时间为: 1,614,576,593,325
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,576,603,325], 实际执行时间为[1,614,576,603,343], 实际误差[18]

回到咱们刚刚的问题上,为何咱们的程序在执行完定时任务后没有正常退出?咱们能够从Java API中对Thread类的描述中找到相关的内容:

从这段描述中,咱们能够看到,只有在两种状况下,Java虚拟机才会退出执行

  1. 手动调用Runtime.exit()方法,而且安全管理器容许进行退出操做
  2. 全部的非守护线程都结束了,要么是执行完run()方法,要么是在run()方法中抛出向上传播的异常

全部的Timer在建立后都会建立关联的工做线程,这个关联的工做线程默认是非守护线程的,因此很明显咱们知足第二个条件,因此程序会继续执行而不会退出。

那么若是咱们将Timer的工做线程设置成守护线程会发生什么呢?

import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序结束时间为: {0}", currentTimeMillis()));
        }));

        Timer timer = new Timer(true);
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序执行时间为: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp + 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任务运行在线程[{0}]上, 指望执行时间为[{1}], 实际执行时间为[{2}], 实际误差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND);
    }
}

程序运行后的实际执行结果

程序执行时间为: 1,614,578,037,976
程序结束时间为: 1,614,578,037,996

能够看到咱们的延迟任务尚未开始执行,程序就已经结束了,由于在咱们的主线程退出后,全部的非守护线程都结束了,因此Java虚拟机会正常退出,而不会等待Timer中全部的任务执行完成后再退出。

2. 执行时间早于当前时间

若是咱们是经过计算Date来指定执行时间的话,那么不可避免会出现一个问题——计算后的时间是早于当前时间的,这很常见,尤为是Java虚拟机会在不恰当的时候执行垃圾回收,并致使STW(Stop the world)。

接下来,咱们将调整以前调用schedule(TimerTask task, Date time)的代码,让它在过去的时间执行

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序结束时间为: {0}", currentTimeMillis()));
        }));

        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序执行时间为: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp - 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任务运行在线程[{0}]上, 指望执行时间为[{1}], 实际执行时间为[{2}], 实际误差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, new Date(startTimestamp - 10 * SECOND));
    }
}

程序运行后的执行结果

程序执行时间为: 1,614,590,000,184
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,589,990,184], 实际执行时间为[1,614,590,000,203], 实际误差[10,019]

能够看到,当咱们指定运行时间为过去时间时,Timer的工做线程会立执行该任务。

可是若是咱们不是经过计算时间,而是指望延迟负数时间再执行,会发生什么呢?咱们将调整以前调用schedule(TimerTask task, long delay)的代码, 让他以负数延迟时间执行

import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序结束时间为: {0}", currentTimeMillis()));
        }));

        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序执行时间为: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp - 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任务运行在线程[{0}]上, 指望执行时间为[{1}], 实际执行时间为[{2}], 实际误差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, -10 * SECOND);
    }
}

程序运行后的执行结果

程序执行时间为: 1,614,590,267,556
Exception in thread "main" java.lang.IllegalArgumentException: Negative delay.
	at java.base/java.util.Timer.schedule(Timer.java:193)
	at cn.mgdream.schedule.Task.main(Task.java:22)

若是咱们传入负数的延迟时间,那么Timer会抛出异常,告诉咱们不能传入负数的延迟时间,这彷佛是合理的——咱们传入过去的时间是由于这是咱们计算出来的,而不是咱们主观传入的。在咱们使用schedule(TimerTask task, long delay)须要注意这一点。

3. 向Timer中添加多个任务

接下来咱们将分别向Timer中添加两个延迟任务,为了更容易地控制两个任务的调度顺序和时间,咱们让第一个任务延迟5秒,第二个任务延迟10秒,同时让第一个任务阻塞10秒后再结束,经过这种方式来模拟出长任务。

import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序执行时间为: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    long exceptedTimestamp = startTimestamp + 5 * SECOND;
                    long executingTimestamp = currentTimeMillis();
                    long offset = executingTimestamp - exceptedTimestamp;
                    System.out.println(format("任务[0]运行在线程[{0}]上, 指望执行时间为[{1}], 实际执行时间为[{2}], 实际误差[{3}]",
                            currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
                    Thread.sleep(10 * SECOND);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 5 * SECOND);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp + 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任务[1]运行在线程[{0}]上, 指望执行时间为[{1}], 实际执行时间为[{2}], 实际误差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND);
    }
}

程序运行后的执行结果

程序执行时间为: 1,614,597,388,284
任务[0]运行在线程[Timer-0]上, 指望执行时间为[1,614,597,393,284], 实际执行时间为[1,614,597,393,308], 实际误差[24]
任务[1]运行在线程[Timer-0]上, 指望执行时间为[1,614,597,398,284], 实际执行时间为[1,614,597,403,312], 实际误差[5,028]

能够看到,两个任务在同个线程顺序执行,而第一个任务由于阻塞了10秒钟,因此是在程序开始运行后的第15秒结束,而第二个任务指望在第10秒结束,可是由于第一个任务尚未结束,因此第二个任务在第15秒开始执行,与与其执行时间误差5秒钟。在使用Timer时尽量不要执行长任务或使用阻塞方法,不然会影响后续任务执行时间的准确性。

4. 周期性执行任务

接下来咱们将会分别使用schedulescheduleAtFixedRate实现周期性执行任务。为了节省篇幅,咱们将只演示如何使用schedule(TimerTask task, long delay, long period)scheduleAtFixedRate(TimerTask task, long delay, long period)来实现周期性执行任务,并介绍它们的差别。而其余的两个方法schedule(TimerTask task, Date firstTime, long period)scheduleAtFixedRate(TimerTask task, Date firstTime, long period)具备相同的效果和差别,就再也不赘述。

首先咱们修改Task类,调用schedule(TimerTask task, long delay, long period)来实现第一次执行完延迟任务后,周期性地执行任务

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        AtomicLong counter = new AtomicLong(0);
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序执行时间为: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long count = counter.getAndIncrement();
                long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任务运行在线程[{0}]上, 指望执行时间为[{1}], 实际执行时间为[{2}], 实际误差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND, SECOND);
    }
}

修改后的代码和使用schedule(TimerTask task, long delay)时的代码基本相同,咱们额外添加计数器来记录任务的执行次数,方法调用添加了第三个参数period,表示任务每次执行时到下一次开始执行的时间间隔,咱们这里设置成1秒钟。

程序运行后的执行结果

程序执行时间为: 1,614,609,111,434
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,609,121,434], 实际执行时间为[1,614,609,121,456], 实际误差[22]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,609,122,434], 实际执行时间为[1,614,609,122,456], 实际误差[22]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,609,123,434], 实际执行时间为[1,614,609,123,457], 实际误差[23]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,609,124,434], 实际执行时间为[1,614,609,124,462], 实际误差[28]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,609,125,434], 实际执行时间为[1,614,609,125,467], 实际误差[33]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,609,126,434], 实际执行时间为[1,614,609,126,470], 实际误差[36]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,609,127,434], 实际执行时间为[1,614,609,127,473], 实际误差[39]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,609,128,434], 实际执行时间为[1,614,609,128,473], 实际误差[39]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,609,129,434], 实际执行时间为[1,614,609,129,474], 实际误差[40]

能够看到,每次任务执行都会有必定时间的误差,而这个误差随着执行次数的增长而不断积累。这个时间误差取决于Timer中须要执行的任务的个数,随着Timer中须要执行的任务的个数增长呈非递减趋势。由于这个程序如今只有一个任务在重复执行,所以每次执行的误差不是很大,若是同时维护成百上千个任务,那么这个时间误差会变得很明显。

接下来咱们修改Task类,调用scheduleAtFixedRate(TimerTask task, long delay, long period)来实现周期性执行任务

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        AtomicLong counter = new AtomicLong(0);
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序执行时间为: {0}", startTimestamp));
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                long count = counter.getAndIncrement();
                long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任务运行在线程[{0}]上, 指望执行时间为[{1}], 实际执行时间为[{2}], 实际误差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND, SECOND);
    }
}

方法scheduleAtFixedRate(TimerTask task, long delay, long period)schedule(TimerTask task, long delay)的效果基本相同,它们均可以达到周期性执行任务的效果,可是scheduleAtFixedRate方法会修正任务的下一次指望执行时间,按照每一次的指望执行时间加上period参数来计算出下一次指望执行时间,所以scheduleAtFixedRate是以固定速率重复执行的,而schedule则只保证两次执行的时间间隔相同

程序运行后的执行结果

程序执行时间为: 1,614,610,372,927
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,610,383,927], 实际执行时间为[1,614,610,383,950], 实际误差[23]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,610,384,927], 实际执行时间为[1,614,610,384,951], 实际误差[24]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,610,385,927], 实际执行时间为[1,614,610,385,951], 实际误差[24]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,610,386,927], 实际执行时间为[1,614,610,386,947], 实际误差[20]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,610,387,927], 实际执行时间为[1,614,610,387,949], 实际误差[22]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,610,388,927], 实际执行时间为[1,614,610,388,946], 实际误差[19]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,610,389,927], 实际执行时间为[1,614,610,389,946], 实际误差[19]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,610,390,927], 实际执行时间为[1,614,610,390,947], 实际误差[20]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,610,391,927], 实际执行时间为[1,614,610,391,950], 实际误差[23]
任务运行在线程[Timer-0]上, 指望执行时间为[1,614,610,392,927], 实际执行时间为[1,614,610,392,946], 实际误差[19]

5. 中止任务

尽管咱们不多会主动中止任务,可是这里仍是要介绍下任务中止的方式。

中止任务的方式分为两种:中止单个任务和中止整个Timer

首先咱们介绍如何中止单个任务,为了中止单个任务,咱们须要调用TimerTaskcancal()方法,并调用Timerpurge()方法来移除全部已经被中止了的任务(回顾咱们以前提到的,过多中止的任务不清空会影响咱们的执行时间)

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        AtomicLong counter = new AtomicLong(0);
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序执行时间为: {0}", startTimestamp));
        TimerTask[] timerTasks = new TimerTask[4096];
        for (int i = 0; i < timerTasks.length; i++) {
            final int serialNumber = i;
            timerTasks[i] = new TimerTask() {
                @Override
                public void run() {
                    long count = counter.getAndIncrement();
                    long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND;
                    long executingTimestamp = currentTimeMillis();
                    long offset = executingTimestamp - exceptedTimestamp;
                    System.out.println(format("任务[{0}]运行在线程[{1}]上, 指望执行时间为[{2}], 实际执行时间为[{3}], 实际误差[{4}]",
                            serialNumber, currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
                }
            };
        }
        for (TimerTask timerTask : timerTasks) {
            timer.schedule(timerTask, 10 * SECOND, SECOND);
        }
        for (int i = 1; i < timerTasks.length; i++) {
            timerTasks[i].cancel();
        }
        timer.purge();
    }
}

首先咱们建立了4096个任务,并让Timer来调度它们,接下来咱们把除了第0个任务外的其余4095个任务中止掉,并从Timer中移除全部已经中止的任务。

程序运行后的执行结果

程序执行时间为: 1,614,611,843,830
任务[0]运行在线程[Timer-0]上, 指望执行时间为[1,614,611,853,830], 实际执行时间为[1,614,611,853,869], 实际误差[39]
任务[0]运行在线程[Timer-0]上, 指望执行时间为[1,614,611,854,830], 实际执行时间为[1,614,611,854,872], 实际误差[42]
任务[0]运行在线程[Timer-0]上, 指望执行时间为[1,614,611,855,830], 实际执行时间为[1,614,611,855,875], 实际误差[45]
任务[0]运行在线程[Timer-0]上, 指望执行时间为[1,614,611,856,830], 实际执行时间为[1,614,611,856,876], 实际误差[46]
任务[0]运行在线程[Timer-0]上, 指望执行时间为[1,614,611,857,830], 实际执行时间为[1,614,611,857,882], 实际误差[52]
任务[0]运行在线程[Timer-0]上, 指望执行时间为[1,614,611,858,830], 实际执行时间为[1,614,611,858,883], 实际误差[53]
任务[0]运行在线程[Timer-0]上, 指望执行时间为[1,614,611,859,830], 实际执行时间为[1,614,611,859,887], 实际误差[57]
任务[0]运行在线程[Timer-0]上, 指望执行时间为[1,614,611,860,830], 实际执行时间为[1,614,611,860,890], 实际误差[60]
任务[0]运行在线程[Timer-0]上, 指望执行时间为[1,614,611,861,830], 实际执行时间为[1,614,611,861,891], 实际误差[61]
任务[0]运行在线程[Timer-0]上, 指望执行时间为[1,614,611,862,830], 实际执行时间为[1,614,611,862,892], 实际误差[62]

咱们能够看到,只有第0个任务再继续执行,而其余4095个任务都没有执行。

接下来咱们介绍如何使用Timercancel()来中止整个Timer的全部任务,其实很简单,只须要执行timer.cancel()就能够。

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序结束时间为: {0}", currentTimeMillis()));
        }));
        
        AtomicLong counter = new AtomicLong(0);
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序执行时间为: {0}", startTimestamp));
        TimerTask[] timerTasks = new TimerTask[4096];
        for (int i = 0; i < timerTasks.length; i++) {
            final int serialNumber = i;
            timerTasks[i] = new TimerTask() {
                @Override
                public void run() {
                    long count = counter.getAndIncrement();
                    long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND;
                    long executingTimestamp = currentTimeMillis();
                    long offset = executingTimestamp - exceptedTimestamp;
                    System.out.println(format("任务[{0}]运行在线程[{1}]上, 指望执行时间为[{2}], 实际执行时间为[{3}], 实际误差[{4}]",
                            serialNumber, currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
                }
            };
        }
        timer.cancel();
    }
}

在将全部的任务添加到Timer后,咱们执行Timer对象的cancel()方法,为了更方便地表现出Timer的工做线程也终止了,咱们注册了生命周期方法,来帮咱们在程序结束后打印结束时间。

程序运行后的执行结果

程序执行时间为: 1,614,612,436,037
程序结束时间为: 1,614,612,436,061

能够看到,在执行Timer对象的cancel()方法后,Timer的工做线程也随之结束,程序正常退出。

源码解析

TimerTask


TimerTask类是一个抽象类,实现了Runnable接口

public abstract class TimerTask implements Runnable

TimerTask对象的成员

首先来看TimerTask类的成员部分

final Object lock = new Object();

int state = VIRGIN;

static final int VIRGIN      = 0;
static final int SCHEDULED   = 1;
static final int EXECUTED    = 2;
static final int CANCELLED   = 3;

long nextExecutionTime;

long period = 0;

对象lock是对外用来控制TimerTask对象修改的锁对象,它控制了锁的粒度——只会影响类属性的变动,而不会影响整个类的方法调用。接下来是state属性表示TimerTask对象的状态。nextExecutionTime属性表示TimerTask对象的下一次执行时间,当TimerTask对象被添加到任务队列后,将会使用这个属性来按照从小到大的顺序排序。period属性表示TimerTask对象的执行周期,period属性的值有三种状况

  1. 若是是0,那么表示任务不会重复执行
  2. 若是是正数,那么就表示任务按照相同的执行间隔来重复执行
  3. 若是是负数,那么就表示任务按照相同的执行速率来重复执行

TimerTask对象的构造方法

Timer对象的构造方法很简单,就是protected限定的默认构造方法,再也不赘述

protected TimerTask() {
}

TimerTask对象的成员方法

接下来咱们看下TimerTask对象的成员方法

public abstract void run();

public boolean cancel() {
    synchronized(lock) {
        boolean result = (state == SCHEDULED);
        state = CANCELLED;
        return result;
    }
}

public long scheduledExecutionTime() {
    synchronized(lock) {
        return (period < 0 ? nextExecutionTime + period
                           : nextExecutionTime - period);
    }
}

首先是run()方法实现自Runnable()接口,为抽象方法,全部的任务都须要实现此方法。接下来是cancel()方法,这个方法会将任务的状态标记为CANCELLED,若是在结束前任务处于被调度状态,那么就返回true,不然返回false。至于scheduledExecutionTime()只是用来计算重复执行的下一次执行时间,在Timer中并无被使用过,再也不赘述。

TimerQueue


TimerQueueTimer维护任务调度顺序的最小优先队列,使用的是最小二叉堆实现,如上文所述,排序用的Key是TimerTasknextExecutionTime属性。

在介绍TimerQueue以前,咱们先补充下数据结构的基础知识

二叉堆(Binary heap)

二叉堆是一颗除了最底层的元素外,全部层都被填满,最底层的元素从左向右填充的彻底二叉树(complete binary tree)。彻底二叉树能够用数组表示,假设元素从1开始编号,下标为i的元素,它的左孩子的下标为2*i,它的右孩子的下标为2*i+1

二叉堆的任意非叶节点知足堆序性:假设咱们定义的是最小优先队列,那么咱们使用的是小根堆,任意节点的元素值都小于它的左孩子和右孩子(若是有的话)的元素值。

二叉堆的定义知足递归定义法,即二叉堆的任意子树都是二叉堆,单个节点自己就是二叉堆。

根据堆序性和递归定义法,二叉堆的根节点必定是整个二叉堆中元素值最小的节点

与堆结构有关的操做,除了add, getMinremoveMin以外,还有fixUpfixDownheapify三个关键操做,而addgetMinremoveMin也是经过这三个操做来完成的,下面来简单介绍下这三个操做

  1. fixUp: 当咱们向二叉堆中添加元素时,咱们能够简单地将它添加到二叉树的末尾,此时从这个节点到根的完整路径上不知足堆序性。以后将它不断向上浮,直到遇到比它小的元素,此时整个二叉树的全部节点都知足堆序性。当咱们减小了二叉堆中元素的值的时候也能够经过这个方法来维护二叉堆。
  2. fixDown: 当咱们从二叉堆中删除元素时,咱们能够简单地将二叉树末尾的元素移动到根,此时不必定知足堆序性,以后将它不断下沉,直到遇到比它大的元素,此时整个二叉树的全部节点都知足堆序性。当咱们增长了二叉堆中元素的值的时候也能够经过这个方法来维护二叉堆。
  3. heapify: 当咱们拿到无序的数组的时候,也能够假设咱们拿到了一棵不知足堆序性的二叉树,此时咱们将全部的非叶节点向下沉,直到整个二叉树的全部节点都知足堆序性,此时咱们获得了完整的二叉堆。这个操做是原地操做,不须要额外的空间复杂度,而时间复杂度是O(N)。

关于二叉堆的详细内容将会在后续的文章中展开详解,这里只作简单的介绍,了解这些咱们就能够开始看TimerQueue的源码。

TimerQueue的完整代码

咱们直接来看TaskQueue的完整代码

class TaskQueue {

    private TimerTask[] queue = new TimerTask[128];

    private int size = 0;

    int size() {
        return size;
    }

    void add(TimerTask task) {
        // Grow backing store if necessary
        if (size + 1 == queue.length)
            queue = Arrays.copyOf(queue, 2*queue.length);

        queue[++size] = task;
        fixUp(size);
    }

    TimerTask getMin() {
        return queue[1];
    }

    TimerTask get(int i) {
        return queue[i];
    }

    void removeMin() {
        queue[1] = queue[size];
        queue[size--] = null;  // Drop extra reference to prevent memory leak
        fixDown(1);
    }

    void quickRemove(int i) {
        assert i <= size;

        queue[i] = queue[size];
        queue[size--] = null;  // Drop extra ref to prevent memory leak
    }

    void rescheduleMin(long newTime) {
        queue[1].nextExecutionTime = newTime;
        fixDown(1);
    }

    boolean isEmpty() {
        return size==0;
    }

    void clear() {
        // Null out task references to prevent memory leak
        for (int i=1; i<=size; i++)
            queue[i] = null;

        size = 0;
    }

    private void fixUp(int k) {
        while (k > 1) {
            int j = k >> 1;
            if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
                break;
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }

    private void fixDown(int k) {
        int j;
        while ((j = k << 1) <= size && j > 0) {
            if (j < size &&
                queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
                j++; // j indexes smallest kid
            if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
                break;
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }

    void heapify() {
        for (int i = size/2; i >= 1; i--)
            fixDown(i);
    }
}

按照咱们以前介绍的二叉堆的相关知识,咱们能够看到TimerQueue维护了TimerTask的数组queue,初始大小size为0。

add操做首先判断了数组是否满了,若是数组已经满了,那么先执行扩容操做,再进行添加操做。如上所述,add操做先将元素放到二叉树末尾的元素(queue[++size]),以后对这个元素进行上浮来维护堆序性。

getMin直接返回二叉树的树根(queue[1]),get方法直接返回数组的第i个元素。removeMin方法会将二叉树末尾的元素(queue[size])移动到树根(queue[1]),并将本来二叉树末尾的元素设置成null,来让垃圾回收器回收这个TimerTask,以后执行fixDown来维护堆序性,quickRemove也是相同的过程,只不过它在移动元素后没有执行下沉操做,当连续执行屡次quickRemove后统一执行heapify来维护堆序性。

rescheduleMin会将树根元素的元素值设置成newTime,并将它下沉到合适的位置。

fixUpfixDownheapify操做就如上文所述,用来维护二叉堆的读序性。不过这里面实现的fixUpfixDown并不优雅,基于交换临位元素的实现须要使用T(3log(N))的时间,而实际上有T(log(N))的实现方法。后续的文章中会详细介绍优先队列与二叉堆的实现方式。

TimerThread


咱们直接来看TimerThread的代码

class TimerThread extends Thread {
    boolean newTasksMayBeScheduled = true;

    private TaskQueue queue;

    TimerThread(TaskQueue queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            mainLoop();
        } finally {
            // Someone killed this Thread, behave as if Timer cancelled
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  // Eliminate obsolete references
            }
        }
    }

    private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    // Wait for queue to become non-empty
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; // Queue is empty and will forever remain; die

                    // Queue nonempty; look at first evt and do the right thing
                    long currentTime, executionTime;
                    task = queue.getMin();
                    synchronized(task.lock) {
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;  // No action required, poll queue again
                        }
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        if (taskFired = (executionTime<=currentTime)) {
                            if (task.period == 0) { // Non-repeating, remove
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { // Repeating task, reschedule
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    if (!taskFired) // Task hasn't yet fired; wait
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)  // Task fired; run it, holding no locks
                    task.run();
            } catch(InterruptedException e) {
            }
        }
    }
}

首先是控制变量newTasksMayBeScheduled,表示当前工做线程是否应该继续执行任务,当它为false的时候它将不会再从任务队列中取任务执行,表示当前工做线程已结束。接下来的queue变量是经过构造方法传进来的任务队列,工做线程的任务队列与Timer共享,实现生产消费者模型。

进入到run()方法,run()方法会调用mainLoop()方法来执行主循环,而finally代码块会在主循环结束后清空任务队列实现优雅退出。

mainLoop()方法中执行了死循环来拉取执行任务,在死循环中首先获取queue的锁来实现线程同步,接下来判断任务队列是否为且工做线程是否中止,若是任务队列为空且工做线程未中止,那么就使用queue.wait()来等待Timer添加任务后唤醒该线程,Object#wait()方法会释放当前线程所持有的该对象的锁,关于wait/notisfy的内容能够去看Java API相关介绍。若是queue退出等待后依旧为空,则表示newTasksMayBeScheduledfalse,工做线程已中止,退出主循环,不然会从任务队列中取出须要最近执行的任务(并不会删除任务)。

取到须要最近执行的任务后,获取该任务的锁,并判断该任务是否已经中止,若是该任务已经中止,那么就把它从任务队列中移除,并什么都不作继续执行主循环。接下来判断当前时间是否小于等于任务的下一次执行时间,若是知足条件则将taskFired设置成true,判断当前任务是否须要重复执行。若是不须要重复执行就将它从任务队列中移除,并将任务状态设置成EXECUTED,若是须要重复执行就根据period设置它的下一次执行时间并从新调整任务队列。

完成这些操做后,若是taskFiredfalse,就让queue对象进入有限等待状态,很容易获得咱们须要的最大等待时间为executionTime - currentTime。若是taskFiredtrue,那么就释放锁并执行被取出的任务。

Timer


Timer对象的成员

首先来看Timer的成员部分

private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);

private final Object threadReaper = new Object() {
    @SuppressWarnings("deprecation")
    protected void finalize() throws Throwable {
        synchronized(queue) {
            thread.newTasksMayBeScheduled = false;
            queue.notify(); // In case queue is empty.
        }
    }
};

private static final AtomicInteger nextSerialNumber = new AtomicInteger(0);

其中queue对象是如前面所说,为了任务调度的最小优先队列。接下来是TimerThread,它是Timer的工做线程,在Timer建立时就已经被分配,并与Timer共享任务队列。

threadReaper是一个只复写了finalize方法的对象,它的做用是当Timer对象没有存活的引用后,终止任务线程,并等待任务队列中的全部任务执行结束后退出工做线程,实现优雅退出。

nextSerialNumber用来记录工做线程的序列号,全局惟一,避免生成的线程名称冲突。

Timer对象的构造方法

接下来咱们看下Timer的全部构造方法

public Timer() {
    this("Timer-" + serialNumber());
}

public Timer(boolean isDaemon) {
    this("Timer-" + serialNumber(), isDaemon);
}

public Timer(String name) {
    thread.setName(name);
    thread.start();
}

public Timer(String name, boolean isDaemon) {
    thread.setName(name);
    thread.setDaemon(isDaemon);
    thread.start();
}

能够看到,全部的构造构造方法所作的事都相同:设置工做线程属性,并启动工做线程。

成员函数

接下来咱们能够看下Timer的成员函数,咱们首先不考虑cancel()purge()方法,直接看schedule系列方法

public void schedule(TimerTask task, long delay) {
    if (delay < 0)
        throw new IllegalArgumentException("Negative delay.");
    sched(task, System.currentTimeMillis()+delay, 0);
}

public void schedule(TimerTask task, Date time) {
    sched(task, time.getTime(), 0);
}

public void schedule(TimerTask task, long delay, long period) {
    if (delay < 0)
        throw new IllegalArgumentException("Negative delay.");
    if (period <= 0)
        throw new IllegalArgumentException("Non-positive period.");
    sched(task, System.currentTimeMillis()+delay, -period);
}

public void schedule(TimerTask task, Date firstTime, long period) {
    if (period <= 0)
        throw new IllegalArgumentException("Non-positive period.");
    sched(task, firstTime.getTime(), -period);
}

public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
    if (delay < 0)
        throw new IllegalArgumentException("Negative delay.");
    if (period <= 0)
        throw new IllegalArgumentException("Non-positive period.");
    sched(task, System.currentTimeMillis()+delay, period);
}

public void scheduleAtFixedRate(TimerTask task, Date firstTime,
                                long period) {
    if (period <= 0)
        throw new IllegalArgumentException("Non-positive period.");
    sched(task, firstTime.getTime(), period);
}

能够看到,全部的schedule方法除了作参数教研外,都将延迟时间和计划执行时间转化为时间戳委托给sched方法来执行。schedulescheduleAtFixedRate传递的参数都相同,不过在传递period参数时使用符号来区分周期执行的方式。

接下来咱们能够看下这位神秘嘉宾——sched方法到底作了哪些事

private void sched(TimerTask task, long time, long period) {
    if (time < 0)
        throw new IllegalArgumentException("Illegal execution time.");

    // Constrain value of period sufficiently to prevent numeric
    // overflow while still being effectively infinitely large.
    if (Math.abs(period) > (Long.MAX_VALUE >> 1))
        period >>= 1;

    synchronized(queue) {
        if (!thread.newTasksMayBeScheduled)
            throw new IllegalStateException("Timer already cancelled.");

        synchronized(task.lock) {
            if (task.state != TimerTask.VIRGIN)
                throw new IllegalStateException(
                    "Task already scheduled or cancelled");
            task.nextExecutionTime = time;
            task.period = period;
            task.state = TimerTask.SCHEDULED;
        }

        queue.add(task);
        if (queue.getMin() == task)
            queue.notify();
    }
}

sched方法首先作了一些参数校验,保证期待执行时间不小于0,且执行周期不至于太大。接下来获取任务队列queue对象的monitor(监视器锁),若是Timer的工做线程已经被中止了,那么就会抛出IllegalStateException来禁止继续添加任务,newTasksMayBeScheduled这个变量将会在稍后介绍。以后sched方法会尝试获取task.lock对象的锁,判断task的状态避免重复添加,并设置task的下一次执行时间、task的执行周期和状态。以后将task添加到任务队列中,若是当前任务就是执行时间最近的任务,那么就会唤起等待queue对象的线程(其实就是thread工做线程)继续执行。

总结

本文从各个方面介绍了java.util.Timer类的使用方式,并从源码角度介绍了java.util.Timer的实现方式。看完本文后,读者应当掌握

  1. 如何执行晚于当前时间的任务
  2. 当任务执行时间早于当前时间会发生什么
  3. 如何向Timer中添加多个任务
  4. 如何周期性执行任务
  5. 如何中止任务
  6. 如何本身实现相似的定时器

但愿本文能够帮助你们在工做中更好地使用java.util.Timer

相关文章
相关标签/搜索