java中的任务调度之Timer定时器(案例和源码分析)

定时器相信大家都不陌生,平时使用定时器就像使用闹钟一样,我们可以在固定的时间做某件事,也可以在固定的时间段重复做某件事,今天就来分析一下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, 2000L, 1000L);
	}
}

指定的流程很简单:

(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 <= 0) throw new IllegalArgumentException("Non-positive period.");
    sched(task, firstTime.getTime(), -period);
}
//第三个
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, 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);
}

这四个方法都执行了同一个方法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,用来解决上面出现的问题。而且也出现了很多优秀的框架。具体的我会在后续文章中介绍。

OK,今天的文章到这,欢迎批评指正。

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值