定时器相信大家都不陌生,平时使用定时器就像使用闹钟一样,我们可以在固定的时间做某件事,也可以在固定的时间段重复做某件事,今天就来分析一下java中自带的定时任务器Timer。

一、Timer基本使用

在Java中为我们提供了Timer来实现定时任务,当然现在还有很多定时任务框架,比如说Spring、QuartZ、Linux Cron等等,而且性能也更加优越。但是我们想要深入的学习就必须先从最简单的开始。

在Timer定时任务中,最主要涉及到了两个类:Timer和TimerTask。他们俩的关系也特别容易理解,TimerTask把我们得业务逻辑写好之后,然后使用Timer定时执行就OK了。我们来看一个最基本的案例:

public class MyTimerTask extends TimerTask {
    private String taskName;
    public MyTimerTask(String taskName) {
        this.taskName = taskName;
    }
    public String getTaskName() {
        return taskName;
    }
    public void setTaskName(String taskName) {
        this.taskName = taskName;
    }
    @Override
    public void run() {
        System.out.println("当前执行的任务是:" + taskName);
    }
}

这就是我们的TimerTask,我们单独写成类时候需要去继承TimerTask。然后呢我们写好了之后就可以使用Timer来执行了。

public class TimerTest {
    public static void main(String[] args) {
        Timer timer = new Timer();
        MyTimerTask myTask = new MyTimerTask("TimerTask 1");
        //在2秒钟之后执行第一次,之后每隔一秒执行一次
        timer.schedule(myTask, 2000L1000L);
    }
}

指定的流程很简单:

(1)第一步:创建一个Timer。

(2)第二步:创建一个TimerTask。

(3)第三步:使用Timer执行TimerTask。

其中第三步无疑是我们目前最关心的,也就是timer.schedule(myTask, 2000L, 1000L)。他的意思是myTask在两秒钟之后开始第一次执行,然后每隔一秒执行一次。这只是最基本的用法。就体现了Timer定时执行的流程。当然java中Timer还为我们提供了很多其他的方法。对此就有必要深入其源码看看了。

二、Timer源码分析

对于一个类的源码分析,我一贯的思路就是先从参数开始,然后构造方法,最后就是常用方法。下面我们就按照这个思路开始今天的源码分析,在这里基于jdk1.8。先给出一张整体类图:

图片

1、参数

Timer的源码中为我们提供了两个最主要的参数TaskQueue和TimerThread

    /**
     * The timer task queue.  This data structure is shared with the timer
     * thread.  The timer produces tasks, via its various schedule calls,
     * and the timer thread consumes, executing timer tasks as appropriate,
     * and removing them from the queue when they're obsolete.
     */

    private final TaskQueue queue = new TaskQueue();
    /**
     * The timer thread.
     */

    private final TimerThread thread = new TimerThread(queue);

上面的代码大概意思是这样的:

(1)TaskQueue:这是一个最小堆,它存放该Timer的所有TimerTask。

(2)TimerThread:执行TaskQueue中的任务,执行完从任务队列中移除。

所以上面这两个参数其实是配合着使用的,那这个TaskQueue是如何存放的呢?在这里我们不妨跟进去看看。

class TaskQueue {
    /**
     * Priority queue represented as a balanced binary heap: the two children
     * of queue[n] are queue[2*n] and queue[2*n+1].  The priority queue is
     * ordered on the nextExecutionTime field: The TimerTask with the lowest
     * nextExecutionTime is in queue[1] (assuming the queue is nonempty).  For
     * each node n in the heap, and each descendant of n, d,
     * n.nextExecutionTime <= d.nextExecutionTime.
     */

    private TimerTask[] queue = new TimerTask[128];
    //增删改查的方法
}

在这里我们只给出了一部分源码,不过这一部分是整个思想原理最核心的,上面英文的大概意思是;TaskQueue是一个平衡二叉堆,具有最小 nextExecutionTime 的 TimerTask 在队列中为 queue[1] ,也就是堆中的根节点。第 n 个位置 queue[n] 的子节点分别在 queue[2n] 和 queue[2n+1] 。不了解二叉堆的话,可以看看数据结构。

也就是说TimerTask 在堆中的位置其实是通过nextExecutionTime 来决定的。nextExecutionTime 越小,那么在堆中的位置越靠近根,越有可能先被执行。而nextExecutionTime意思就是下一次执行开始的时间。

还有一个TimerTask数组,默认大小是128个。

2、构造方法

构造方法就比较简单了,这里一共有四个:

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();
}

(1)第一个:默认构造方法。

(2)第二个:在构造器中指定是否是守护线程。

(3)第三个:带有名字的构造方法。

(3)第四个:不仅带名字,还指定是否是守护线程。

不过我们需要注意一点的是,Timer在构造完成之后会启动一个后台线程用于执行TaskQueue里面的TimerTask 。

3、定时任务方法

在一开始我们提到,我们不仅可以在指定的时间执行某些任务,还可以在一段时间之后执行。我们对这些方法进行总结一下:

(1)schedule(task,time) 
在时间等于或超过time的时候执行且只执行一次task,这个time表示的是例如2019年11月11日上午11点11分11秒。指的是时刻。

(2)schedule(task,time,period)

在时间等于或超过time的时候首次执行task,之后每隔period毫秒重复执行一次task 。这个time和上一个一样。

(3)schedule(task, delay)

在delay时间之后,执行且只执行一次task。这个delay表示的是延迟时间,比如说三秒后执行。

(4)schedule(task,delay,period)

在delay时间之后,开始首次执行task,之后每隔period毫秒重复执行一次task ,这个delay和上面的一样。

我们不如来看看源码:

//第一个:
public void schedule(TimerTask task, Date time) {
    sched(task, time.getTime(), 0);
}
//第二个:
public void schedule(TimerTask task, Date firstTime, long period) {
    if (period <= 0throw new IllegalArgumentException("Non-positive period.");
    sched(task, firstTime.getTime(), -period);
}
//第三个
public void schedule(TimerTask task, long delay) {
    if (delay < 0throw new IllegalArgumentException("Negative delay.");
    sched(task, System.currentTimeMillis()+delay, 0);
}
//第四个:
public void schedule(TimerTask task, long delay, long period) {
    if (delay < 0throw new IllegalArgumentException("Negative delay.");
    if (period <= 0throw new IllegalArgumentException("Non-positive period.");
    sched(task, System.currentTimeMillis()+delay, -period);
}

这四个方法都执行了同一个方法sched,所以我们要弄清楚原理,就必须要再跟进去看看:

   private void sched(TimerTask task, long time, long period) {
        if (time < 0)
            throw new IllegalArgumentException("Illegal execution time.");
        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();
        }
    }

上面的代码我们来分析一下,最上面的if就是排除一下异常情况,最核心的就是synchronized里面的代码。首先将任务添加到队列中,然后根据nextExecutionTime调整队列。

添加任务add(task):

void add(TimerTask task) {
    if (size + 1 == queue.length)
        //添加任务很简单,就是数组的拷贝
        queue = Arrays.copyOf(queue, 2*queue.length);
    queue[++size] = task;
    fixUp(size);//维护最小堆
}

维护最小堆:

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;
    }
}

上面就是Timer中如何执行的定时任务核心,但是还有一个方法,也是执行定时任务的。叫scheduleAtFixedRate

下面我们来分析一下,然后比较和上面的不同。

4、scheduleAtFixedRate方法

这个方法有两个:

(1)scheduleAtFixedRate(task, time, period)

在时间等于或超过time的时候首次执行task,之后每隔period毫秒重复执行一次task 。这个time表示的是例如2019年11月11日上午11点11分11秒。指的是时刻。

(2)scheduleAtFixedRate(task, delay, period)

在delay时间之后,开始首次执行task,之后每隔period毫秒重复执行一次task ,这个delay表示的是延迟时间,比如说三秒后执行。

既然上面都已经有了4个定时器,为什么这里还要再增加几个呢?我们来分析一下他们的区别:

分两种情况:
① 首次计划执行的时间  
schedule:如果第一次执行时间被delay了,随后的执行时间按照上一次实际执行完的时间点进行计算 。
scheduleAtFixedRate:如果第一次执行时间被delay了,随后的执行时间按上一次开始的时间进行计算,并且为了赶上进度会多次执行任务,因此TimerTask中的执行体需要考虑同步。

②任务执行所需时间 
schedule方法:下一次执行时间会不断延后,因此参照的是上一次执行完成的时间点。
scheduleAtFixedRate方法:下一次执行时间不会延后,因此存在并发性。
我们可以看一下图:

图片

5、其他方法

我们已经明白了如何创建Timer和执行定时任务,如果在执行的时候我们突然改变主意,想要取消怎么办呢?这里Timer当然为我们提供了。

(1)cancel:取消此计时器任务。

(2)scheduledExecutionTime():返回此任务最近实际执行的安排执行时间。

6、任务调度

任务调度也就是说我们的线程如何去执行这些任务。其实在TimerThread调用了run来执行,我们看一下源码。

public void run() {
    try {
        mainLoop();
        } finally {
        synchronized(queue) {
            newTasksMayBeScheduled = false;
            queue.clear();  
        }
    }
}

也就是说其实真正执行任务调度的是mainLoop(),synchronized代码块只是为了确保在执行完之后能够移除这个task。

而这个mainLoop方法的思想很简单,就是拿出任务队列中的第一个任务,如果执行时间还没有到,则继续等待,否则立即执行。源码在这里就不再给出了。

三、Timer缺陷

上面从源码的角度分析了一下Timer,因为用法很简单,主要是源码分析。说了这么多,Timer还是有一定的缺陷的,

1、Timer管理延时任务的缺陷

Timer在执行定时任务时只会创建一个线程,所以如果存在多个任务,且任务时间过长,超过了两个任务的间隔时间,会发生一些缺陷。我们看一个例子:

这个例子中的功能是这样的,第一个任务在1秒钟之后开始执行,第二个任务在2秒钟之后开始执行。

第一步:定义两个TimerTask

public class Task1 extends TimerTask {
    @Override
    public void run() {
        try {
            SimpleDateFormat sdf = 
                    new SimpleDateFormat("hh:MM:ss");
            String data = sdf.format(new Date());
            System.out.println("task 1:"+data);
            //休眠了3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

还有一个:

public class Task2 extends TimerTask {
    @Override
    public void run() {
        SimpleDateFormat sdf = 
                new SimpleDateFormat("hh:MM:ss");
        String data = sdf.format(new Date());
        System.out.println("task 2:"+data);
    }
}

第二步:我们测试一下:

public class TimerTest {
    public static void main(String[] args) {
        Timer timer = new Timer();
        Task1 myTask1 = new Task1();
        Task2 myTask2 = new Task2();
        SimpleDateFormat sdf = 
                new SimpleDateFormat("hh:MM:ss");
        String data = sdf.format(new Date());
        System.out.println("Main :"+data);
        timer.schedule(myTask1, 1000L);
        timer.schedule(myTask2, 2000L);
    }
}
//Main :05:09:30
//task 1:05:09:31
//task 2:05:09:34

我们在上面的Task1中会发现,任务2不是应该在32秒的时候执行嘛,怎么会在4秒钟之后才执行。究其原因是任务1执行了3秒,但是线程只有一个,所以只能先把任务1执行完才去执行任务2。这就是其缺陷之一。

2、Timer当任务抛出异常时的缺陷

这个缺陷的意思是,其中有一个任务抛出了RuntimeException,那么所有的任务都会停止执行。这个演示起来很简单。

第一步:声明几个定时任务

public class Task1 extends TimerTask {
    @Override
    public void run() {
        throw new RuntimeException();
    }
}
public class Task2 extends TimerTask {
    @Override
    public void run() {
        System.out.println("任务2执行");
    }
}

第二步:测试

public class TimerTest {
    public static void main(String[] args) {
        Timer timer = new Timer();
        Task1 myTask1 = new Task1();
        Task2 myTask2 = new Task2();
        timer.schedule(myTask1, 1000L);
        timer.schedule(myTask2, 2000L);
    }
}

我们来看一下结果:

图片

正是Timer有很多的缺陷,所以出现了Timer的替代品ScheduledExecutorService,用来解决上面出现的问题。而且也出现了很多优秀的框架。具体的我会在后续文章中介绍。


更多相关文章

  1. 设计模式之模板方法模式
  2. Java实现定时任务的三种方法
  3. 无限重置idea试用期过期时间插件 简单方便 亲测可用
  4. 数据结构--时间复杂度与空间复杂度
  5. Java 8 日期 / 时间( Date Time )API 指南
  6. 使用 ThreadLocal 变量的时机和方法
  7. clone 方法是如何工作的
  8. Java EE 8 时间表公布,预计 7 月发布最终版

随机推荐

  1. android 的View Tree和 DecorView(Android
  2. android edittext 隐藏键盘
  3. Android事件处理
  4. Android和H5的交互
  5. android 退出程序 seekbar mediaplayer
  6. Android RecyclerView DividerItemDecora
  7. ch010 Android GridView
  8. Android Email程序源码
  9. android内存机制
  10. Android(安卓)Jetpack系列——ViewModel