前言

在平时的开发中,肯定需要使用定时任务,而 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. iOS笔记之UIKit_UISlider/UIStepper/UISwitch

    - (void)viewDidLoad { [super viewDidLoad]; self.sp = [[UIStepper alloc]init]; //设置计步器的位置 self.sp.cen ...

  2. [mysql语句] mysql 语句收集

    // http://stackoverflow.com/questions/6666152/mysql-order-by-where 1. "select * from t_activity ...

  3. Apollo 配置详细步骤(Windows环境)

    一. 准备工作 1.下载 apollo 安装包 下载链接:http://activemq.apache.org/apollo/download.html 2.下载 JavaJDK 安装包 ( apol ...

  4. Event Tracing For Windows

    https://blogs.msdn.microsoft.com/oanapl/2009/08/04/etw-event-tracing-for-windows-what-it-is-and-usef ...

  5. MD5状态变量,为什么是A=0x67452301,B=0xefcdab89,C=0x98badcfe,D=0x10325476这几个变量

    找到过一篇答复: 3.3 Step 3. Initialize MD Buffer A four-word buffer (A,B,C,D) is used to compute the messag ...

  6. python操作excel及json

    有一个存着学生成绩的文件:stuscore.txt,里面存的是json串,json串读起来特别不直观,需要你写代码把它都写到excel中,并计算出总分和平均分,json格式如下: { ":[ ...

  7. [leetcode.com]算法题目 - Length of Last Word

    Given a string s consists of upper/lower-case alphabets and empty space characters ' ', return the l ...

  8. 《Python自动化运维之路》 业务服务监控(二)

    文件内容差异对比方法 使用diffie模块实现文件内容差异对比.dmib作为 Python的标准库模块,无需安装,作用是对比文本之间的差异,且支持输出可读性比较强的HTML文档,与 Linux下的di ...

  9. tomcat 启动 证书异常java.io.IOException: Alias name [cas] does not identify a key entry

    在搭建CAS server的过程中,Tomcat开启https,配置秘钥证书,证书是通过keytool生成的 <Connector port=" protocol="org. ...

  10. linux定时重启tomcat服务的脚本学习

    要求:在linux中定时重启一个tomcat服务 一:shell脚本即Shell Script [1],Shell脚本与Windows/Dos下的批处理相似,也就是用各类命令预先放入到一个文件中,方便 ...