前言

在平时的开发中,肯定需要使用定时任务,而 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. 论EFMS模拟量部分采集电路的修改

    论1:电阻R11的作用 如图1是2014-3-11之前模拟量采集的部分硬件电路,图2是纠正后的正确电路. D5是SA20CA,TVS双向二极管,有效防止外接电源的浪涌冲击情况,保护电路.  D6是稳压 ...

  2. SQLSqlserver中如何将一列数据,不重复的拼接成一个字符串

    把一列数据拼接成一个字符串比较简单: declare @test varchar(500) set @test=''; select @test=@test+name+',' from person ...

  3. 深入理解Aspnet Core之Identity(5)

    主题 本篇我将会介绍验证用户的机制当账户被创建的时候,同样 这个过程主要有IUserValidator这个接口来实现的,和密码验证一样Identity同样也内置已经实现好的账户验证.账户验证的比较简单 ...

  4. C#之WinForm设置控件居中

    简单阐述 在C#的WinForm里面,原生控件是没有居中属性的,故通过重写OnResize(EventArgs e)方法,通过计算,重新定位控件位置. 以Label控件为例 (1)将label的Aut ...

  5. AEAI DP开发统计分析

    1 背景概述 平时做统计分析都是调rest服务,给前台提供数据,然后在管理控制台里配置portlet.但并不是所有的项目都会用到portal,这时就需要在AEAI DP应用开发平台里开发统计分析了,下 ...

  6. UVA10829 L-Gap Substrings(后缀数组+ST表)

    后缀数组+ST表. 代填的坑. \(Code\ Below:\) #include <bits/stdc++.h> #define ll long long using namespace ...

  7. ReactNatvie遇到的错误

    1:新版的React包中没有包含PropTypes,如果使用需要从‘prop-types’包中导入. 2: 'prop-types'包中直接定义‘PropTypes.style’是无效的,需要使用‘P ...

  8. C# 中 DataTable转换成IList

    在用C#作开发的时候经常要把DataTable转换成IList:操作DataTable比较麻烦,把DataTable转换成IList,以对象实体作为IList的元素,操作起来就非常方便. 注意:实体的 ...

  9. spring mvc 使用kaptcha配置生成验证码实例

    SpringMVC整合kaptcha(验证码功能) 一.依赖 <dependency> <groupId>com.github.penggle</groupId> ...

  10. Shell - 简明Shell入门05 - 条件语句(Case)

    示例脚本及注释 #!/bin/bash var=$1 # 将脚本的第一个参数赋值给变量var case $var in right) echo "Right!";; wrong) ...