Timer与TimerTask的基本使用和源码解析

前言

Timer是一种定时器工具,是Java提供的原生Scheduler(任务调度)工具类,不需要导入其他jar包,使用方便,用来在一个后台线程计划执行指定任务。它可以安排任务“执行一次”或者定期“执行多次”。如需要实现在某个具体时间执行什么任务的话,Timer足以胜任 。

核心概念

在正式编程之前,有必要了解几个核心的概念,有助于对后续代码的编程和理解:
1.Timer其实就是一个调度器,构造函数如下:

// 无参构造方法,关联线程没有作为守护程序运行
    public Timer() {
        this("Timer-" + serialNumber());
    }
// 有参构造方法,true: 表示关联线程作为守护程序运行,false: 表示关联线程没有作为守护程序运行
// 作为守护程序当且仅当进程结束时,自动注销
   public Timer(boolean isDaemon) {
        this("Timer-" + serialNumber(), isDaemon);
    }
// 传入Timer的名称
    public Timer(String name) {
        thread.setName(name);
        thread.start();
    }
// 传入Timer的名字和是否作为守护程序的参数
    public Timer(String name, boolean isDaemon) {
        thread.setName(name);
        thread.setDaemon(isDaemon);
        thread.start();
    }

2.TimerTask是定时任务,其实现了一个Runnable的run方法的一个实现类,要完成的定时任务要在run方法中执行。
3.timer对象是原子性的,timer是单线程的,在执行多个任务调度时得等其他任务执行完 ,多任务执行线程等待,一个timer对象在执行多个任务时使用的是同一线程。
4.timer的几个方法:在这里插入图片描述

  • 这个方法是调度一个timerTtask,经过delay(ms)后开始进行调度,仅仅调度一次。
public void schedule(TimerTask task, long delay)
  • 在指定的时间点time上调度一次。
public void schedule(TimerTask task, Date time)
  • 这个方法是调度一个timerTask,在delay(ms)后开始调度,每次调度完成以后,最少等待period(ms)后才开始调度
public void schedule(TimerTask task, long delay, long period)
  • 这个方法是调度一个timerTask,在第一次调度完后,最少等待period(ms)后才开始调度(和上一个方法唯一的区别就是第二个传入的参数是第一次调度的时间)
public void schedule(TimerTask task, Date firstTime, long period)
  • 这个方法是安排指定的任务在指定的时间开始进行重复的固定速率执行(第二个参数是第一次调度的时间)
public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period)
  • 这个方法是安排指定的任务在指定的延迟时间片后开始进行重复的固定速率执行(和上一个方法唯一不同的地方就是第二个参数是延迟的时间片段)
public void scheduleAtFixedRate(TimerTask task, long delay, long period)
  • 这个方法是终止定时器,丢弃所有当前已经安排的定时任务
public void cancel()
  • 这个方法是从此计时器的任务队列中移除所有已取消的定时任务。
public int purge()

接下来让我们正式开始肝代码吧
1、先验证下timer是单线程

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

public class TestTimer {
    public static void main(String[] args) {
        // 创建 timer对象
        Timer timer = new Timer();
        // 执行定时任务1
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "定时任务1");
            }
        }, 1000);
        // 执行定时任务2
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "定时任务2");
                // 取消定时任务
                  timer.cancel();
            }
        }, new Date());
        System.out.println(Thread.currentThread().getName() + "主线程");
    }
}

上面代码的意思就是有两个定时任务,一个是延迟时间片段,一个指定了时间执行定时任务,并且打印了当前线程的name.
下面看下运行结果:
在这里插入图片描述
从运行结果可以看出:
(1)、线程Timer-0先执行了定时任务2,然后定时任务未到达时间片段进行等待,但是明显可以看出一个timer对象在执行多任务使用的是同一线程。
(2)、大家可以看到程序仍处于运行状态,其实主线程运行完已经结束了,处于运行态的是定时任务创建的线程。倘若在一个定时任务结束时进行cancel(),那么还没来得及执行的定时任务也无机会执行,此时定时任务创建的线程也结束了;

源码解析

 private final TaskQueue queue = new TaskQueue();
  • 一看名字就知道这是一个任务队列,大概就是要完成的调度任务吧。
	class TimerThread extends Thread {......}
     /**
     * The timer thread.
     */
    private final TimerThread thread = new TimerThread(queue);
  • Timer内封装了一个线程,继承于Thread,类型是default,所以默认情况下是引用不到的,为Timer自我使用,做独立于外部线程的调度。
private final Object threadReaper = new Object() {
        protected void finalize() throws Throwable {
            synchronized(queue) {
                thread.newTasksMayBeScheduled = false;
                queue.notify(); // In case queue is empty.
            }
        }
    };
  • 再者就是这个Object类型的threadReaper属性了,它其实只是Override了finalize()方法而已,在垃圾回收的时候起作用,做GC的回补(关于垃圾回收大家可以自己先去了解)。
接下来看下调度方法的在源码中是如何实现的:
 public void schedule(TimerTask task, Date time) {
        sched(task, time.getTime(), 0);
    }
 public void schedule(TimerTask task, long delay) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        sched(task, System.currentTimeMillis()+delay, 0);
    }
  • 对比以上两个方法,第一个参数都是将定时任务task传入,第二个参数前者传入的时间点,第一次执行定时任务开始点是直接取对象的getTime()即可,后者传入的延迟时间片段,第一次执行的定时任务的开始点是当前系统时间毫秒数+delay(延迟时间片段),关于在调用sched方法时的第三个参数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 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);
    }
  • 看到这两个方法此刻就觉得很有意思了,两个方法逻辑除了period有正负号之外其他并无其他区别,此刻也可以大胆猜测上面的0也是代表时间片的意思。其实当阅读完源码你就会惊奇的发现传入的period如过是一个负值时,当取反后恰是和调用scheduleAtFixedRate方法功能一样,而调用schedule时和调用scheduleAtFixedRate的功能也一样,因此断定应该是用来做参数的区分。

  • 那么现在看下queue和sched方法的具体实现:

class TaskQueue {
    private TimerTask[] queue = new TimerTask[128];
    private int size = 0;
    ......
    }
    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();
        }
    }

我们可以看到queue结构很简单,就是一个数组和初始容量大小128,它也是能进行扩容的,只是在扩容时进行内存拷贝而已。它也提供了add(TimerTask)size()getMin()get(int)removeMin()quickRemove(int)rescheduleMin(long newTime)isEmpty()clear()fixUp()fixDown()heapify()这些方法,具体就不在此一一解释了。

在sched方法实现时可以看到用synchronized进行同步,所以说在timer这个级别是线程安全的。最后在把task做为参数进行一系列的赋值操作,

      task.nextExecutionTime = time;  //下一次执行执行的时间
      task.period = period;  //时间片
      task.state = TimerTask.SCHEDULED;  //状态

然后将它放入queue队列中,做一次notify操作。

  • 下面我们对cancel()方法和purge()方法进行分析:
 public void cancel() {
        synchronized(queue) {
            thread.newTasksMayBeScheduled = false;
            queue.clear();
            queue.notify();  // In case queue was already empty.
        }
    }
public int purge() {
         int result = 0;
         synchronized(queue) {
             for (int i = queue.size(); i > 0; i--) {
                 if (queue.get(i).state == TimerTask.CANCELLED) {
                     queue.quickRemove(i);
                     result++;
                 }
             }

             if (result != 0)
                 queue.heapify();
         }

         return result;
     }
}

cancel()方法是其实就是一个取消操作,在上面测试时也有提过,一旦进行cancel操作timer就会结束掉。这里貌似看起来只是将newTasksMayBeScheduled 设置为了fasle,最后进行clear和notify操作,但并没有进行线程结束的操作。这就要从TimerThread类说起了,在这个类之前,当进行cancel之后,此时通过调用purge方法实现对这些cancel掉的进行空间回收。此时会造成乱序,那么就要调用heapify()进行重新排序。其实这个调度就是TimerThread类,内部有一个属性是:newTasksMayBeScheduled,也就是上面所提及的那个参数在cancel的时候会被设置为false。

  • 接下来继续对TimerThread类进行分析:
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
            }
        }
    }

看到源码可以明白所调用的queue就是这里通过构造方法传入进来的,传入后进行赋值操作,然后timer对象将其传入给这个线程,此时执行的自然是run()方法了。run()方法里try{}部分就一个mainLoop()方法,一看便知这是一个主循环程序,finally中也就是必然执行的程序为将参数newTasksMayBeScheduled为false,并将队列清空掉。
其实最核心的部分也就是mainLoop()了:

 /**
     * The main timer loop.  (See class comment.)
     */
    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) {
            }
        }
    }
}

可以看出timer就是一个死循环的程序,除非发生了异常被捕捉或者在特定条件下被break后才会跳出循环。

   // Wait for queue to become non-empty
        while (queue.isEmpty() && newTasksMayBeScheduled)
              queue.wait();          

循环条件为queue为空且newTasksMayBeScheduled状态为true的时候。跳出循环的条件就是队列不为空或者newTasksMayBeScheduled为false才会跳出。wait就是在等待其他地方对queue进行notify操作,从上面的代码中可以发现,当发生add、cancel以及在threadReaper调用finalize方法的时候会被调用,其实基本可以不考虑,发生add的时候也就是当队列还是空的时候,发生add使得队列不为空就跳出循环,而cancel是设置了newTasksMayBeScheduled=false,否则不会进入这个循环。

  if (queue.isEmpty())
      break; // Queue is empty and will forever remain; die

跳出循环后,若进行了newTasksMayBeScheduled=false跳出,那么就是调用了cancel操作。此时queue就是空的,就直接跳出外部的死循环,所以cancel就是这样实现的,倘若下还有任务在跑但是还没运行到此处,cancel是没有意义的。
接下来就是判断下一次执行的时间是否小于当前的系统时间,若小于则开始执行。然后判断时间片是否等于0,若为0,则调用removeMin()方法将其移除,否则将task通过rescheduleMin()设置最新时间并进行排序。
下面又回到了period正、负、0的问题了,当period为负数时,则会进行当前系统时间+时间片来计算下一次的执行时间,当period为正时,则会进行执行时间加上时间片来计算下一次的执行时间,这其实就是schedule和scheduleAtFixedRate的区别,其实内部是通过正负数进行判定的,也许底层大哥是不想增加参数。恰到好处的做到如果在schedule方法传入负数和scheduleAtFixedRate的功能是一样的,反之在scheduleAtFixedRate方法中传入负数功能和schedule方法是一样的。当period为0时就只执行一次。此乃妙哉!

if (!taskFired)// Taskhasn't yet fired; wait
    queue.wait(executionTime- currentTime);

若task的执行时间还未到,那么就需要等待一段时间。当然其中也可能被cancel操作给唤醒,因为内部有notify()方法。

if (taskFired)  // Task fired; run it, holding no locks
    task.run();

最后就是如果task要执行了,那么就该调用run()方法了。在这里要说明下,虽然实现了Runnable接口,但是timer并不是再启动一个新的线程或者从线程池中获取一个线程来执行其他的任务,所以这里的run()方法只是为了执行而已,并无其他特殊的意义。

拓展

目前再实际的开发过程当中,用到的实际比较少;一般都会使用 Quartz,它是一个完全由Java 编写的开源调度框架。下章将对目前业务中使用的Quart进行分析。

以上是个人实践后的总结,有新的见解请指教,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值