前言

在平时的开发中,肯定需要使用定时任务,而 Java 1.3 版本提供了一个 java.util.Timer 定时任务类。今天一起来看看这个类。

1.API 介绍

Timer 相关的有 3 个类:

Timer :面向程序员的API 都在这个类中。

TaskQuue: 存储任务。

TimerThread: 执行任务的线程。

这个类的构造方法有 4 个:

Timer()                               创建一个新计时器。
Timer(boolean isDaemon) 创建一个新计时器,可以指定其相关的线程作为守护程序运行。
Timer(String name) 创建一个新计时器,其相关的线程具有指定的名称。
Timer(String name, boolean isDaemon) 创建一个新计时器,其相关的线程具有指定的名称,并且可以指定作为守护程序运行。

程序员可以使用的 API 如下:

void cancel()                                                          终止此计时器,丢弃所有当前已安排的任务。
int purge() 从此计时器的任务队列中移除所有已取消的任务。
void schedule(TimerTask task, Date time) 安排在指定的时间执行指定的任务。
void schedule(TimerTask task, Date firstTime, long period) 安排指定的任务在指定的时间开始进行重复的固定延迟执行。
void schedule(TimerTask task, long delay) 安排在指定延迟后执行指定的任务。
void schedule(TimerTask task, long delay, long period) 安排指定的任务从指定的延迟后开始进行重复的固定延迟执行。
void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) 安排指定的任务在指定的时间开始进行重复的固定速率执行。
void scheduleAtFixedRate(TimerTask task, long delay, long period) 安排指定的任务在指定的延迟后开始进行重复的固定速率执行。

下面从几个具有代表性的方法开始分析 Timer 的源码。

2. 从构造方法开始

Timer timer = new Timer();

public Timer() {
this("Timer-" + serialNumber());
} public Timer(String name) {
thread.setName(name);
thread.start();
} /**
* The timer thread.
*/
private final TimerThread thread = new TimerThread(queue); private final TaskQueue queue = new TaskQueue(); private TimerTask[] queue = new TimerTask[128];

从上面一连串的构造方法中,可以看出,Timer 内部使用了一个线程 TimerThread,线程的构造参数是一个队列(数组)。

然后直接启动了这个线程,默认是非守护模式的。

而这个线程的 run 方法又是如何的呢?

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

主要执行 mainLoop 方法,当任务结束后,清除队列。并不在接受新的任务。

那么这个 mainLoop 方法的逻辑是什么呢?猜想一下,肯定是执行队列中的任务。

private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// 如果队列是空 且 newTasksMayBeScheduled 是 true,阻塞等待
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) {
// 如果有中断异常就忽略。
}
}
}

一如既往,写了很多注释,简单说说逻辑:

  1. 死循环并锁住队列,因为这个 Timer 对象可能会被多个线程使用。
  2. 从队列中取出任务。如果任务是重复执行的,就重新设置任务的执行时间。
  3. 执行任务的 run 方法。

这里有几个注意的地方:

  1. 该方法忽略了线程中断异常。当 wait 方法中断异常的时候,是不起作用的。
  2. 该方法值只捕获线程中断异常,如果发生了其他异常,整个 Timer 就会停止。

So,一定不要在自己的任务里抛出异常,否则一定会影响整个定时任务。

3. schedule 方法

timer.schedule(new MyTask(), 1000, 2000);

以上定义了一个任务,1 秒后执行,重复执行时间 2 秒。

schedule 代码如下:

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

在 dealy 时间的基础上,加上了当前时间,将 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) {
// 如果该变量是 false ,说明任务线程停止了,抛出异常
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();
}
}

总结一下该方法,将任务添加进队列,如果调度线程结束了,就抛出异常—— 不能再添加。如果添加成功之后,获取到的第一个任务就是这个任务,说明调度线程阻塞了,那就唤醒他。

4.总结

从一个定时任务的角度讲,Timer 非常的简单,使用一个线程,使用一个队列。在简单的场合,Timer 确实能够满足需求,但 Timer 还是有很多的缺陷:

  1. 不能 catch 住非线程中断异常,如果用户任务异常,将会导致整个 Timer 停止。

  2. 默认情况下不是守护线程,也就是说,他会阻止应用程序停止。你可以使用 cancel 方法停止他。

  3. 如果 Timer 因为 stop 方法获取用户任务异常终止了,那么将再也不能向队列中添加任务了。否则抛出异常。

  4. 如果某个任务的执行时间太长,那么他将会 “独占” 计时器的任务执行现场。导致延迟后续任务的执行,并且会将任务 “堆” 在一起。

So, 大规模的生产环境中,不建议使用 Timer,而是使用 JUC 的 ScheduledThreadPoolExecutor。楼主将在后面的文章中分析 ScheduledThreadPoolExecutor 的实现,相比较 Timer 有什么好处。

并发编程 —— Timer 源码分析的更多相关文章

  1. Java并发编程-ReentrantLock源码分析

    一.前言 在分析了 AbstractQueuedSynchronier 源码后,接着分析ReentrantLock源码,其实在 AbstractQueuedSynchronizer 的分析中,已经提到 ...

  2. Java并发编程 ReentrantLock 源码分析

    ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大. 这个类主要基于AQS(Abst ...

  3. 并发编程—— FutureTask 源码分析

    1. 前言 当我们在 Java 中使用异步编程的时候,大部分时候,我们都会使用 Future,并且使用线程池的 submit 方法提交一个 Callable 对象.然后调用 Future 的 get ...

  4. Java并发编程-AbstractQueuedSynchronizer源码分析

    简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过 ...

  5. Java并发编程 LockSupport源码分析

    这个类比较简单,是一个静态类,不需要实例化直接使用,底层是通过java未开源的Unsafe直接调用底层操作系统来完成对线程的阻塞. package java.util.concurrent.locks ...

  6. java 并发编程——Thread 源码重新学习

    Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...

  7. Unity时钟定时器插件——Vision Timer源码分析之二

      Unity时钟定时器插件——Vision Timer源码分析之二 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 前面的已经介绍了vp_T ...

  8. 并发工具CyclicBarrier源码分析及应用

      本文首发于微信公众号[猿灯塔],转载引用请说明出处 今天呢!灯塔君跟大家讲: 并发工具CyclicBarrier源码分析及应用 一.CyclicBarrier简介 1.简介 CyclicBarri ...

  9. Java异步编程——深入源码分析FutureTask

    Java的异步编程是一项非常常用的多线程技术. 之前通过源码详细分析了ThreadPoolExecutor<你真的懂ThreadPoolExecutor线程池技术吗?看了源码你会有全新的认识&g ...

随机推荐

  1. Tencent interview

    1.常见的聚类算法 1):划分法:k-means 2):基于密度的方法: 2.EM 算法 EM算法是在概率模型中寻找参数的最大似然估计或者最大后验概率的算法,其中概率模型依赖于无法观测的隐藏变量.EM ...

  2. 2.虚拟机安装的ubuntu全屏显示

    虚拟机下面安装了ubuntu系统,显示的屏幕只有那么一小块儿,不知道如何才能全屏,那么如何全屏呢?且看下面经验. 方法/步骤 打开虚拟机,并点击要更改成全屏的那个ubuntu系统的电源 我的虚拟机名字 ...

  3. WPF Adorner

    之前做项目时,为了实现类似微信消息数目的效果   image.png ,我之前是修改的ControlTemplate.类似于将一个带数字的控件,放在另一个控件的右上角,来实现的这个效果. 原来WPF有 ...

  4. PHP 中 var_export、print_r、var_dump 调试中的区别

    1.output basic type 代码 $n = "test"; var_export($n); print_r($n); var_dump($n); echo '----- ...

  5. javascript之快速排序

    快速排序思想其实还是挺简单的,分三步走: 1.在数组中找到基准点,其他数与之比较. 2.建立两个数组,小于基准点的数存储在左边数组,大于基准点的数存储在右边数组. 3.拼接数组,然后左边数组与右边数组 ...

  6. day 38 jq 入门 学习(一)

    前情提要: jq是简化版本的js 可以把很多很复杂的js 提炼让前端代码更好写 一:jq的使用 <!DOCTYPE html> <html lang="en"&g ...

  7. 线性表java实现

    顺序表 public class SequenceList { /* * content,节点内容 * location,节点在表中的位置(序号) * */ private String conten ...

  8. WebForm——JS检测浏览器是否是IE浏览器

    function IEVersion() { var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串 && userAgen ...

  9. 前端安全 -- XSS攻击

    XSS漏洞是最广泛.作用最关键的web安全漏洞之一.在绝大多数网络攻击中都是把XSS作为漏洞链中的第一环,通过XSS,黑客可以得到的最直接利益就是拿到用户浏览器的cookie,从而变相盗取用户的账号密 ...

  10. War文件部署

    其实,开始要求将源码压缩成War文件时,一头雾水! 公司项目要求做CAS SSO单点登录 也就是这玩意.... 其实war文件就是Java中web应用程序的打包.借用一个老兄的话,“当你一个web应用 ...