前言

在平时的开发中,肯定需要使用定时任务,而 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. 所有网卡常用信息获取集中展示(CentOS6 &CentOS7)

    查看所有网卡,状态.光电类型.ip.广播地址.掩码 1.命令如下 ( string='|%-3s|%-18s|%-10s|%-10s|%-10s|%-16s|%-16s|%-16s|'; br=&qu ...

  2. Android-Retrofit-2.0-Post与Get-请求有道词典翻译

    Retrofit-2.0版本后,内置已经集成了OKHttp,在使用Retrofit的时候 看似是Retrofit去网络请求的 实际上Retrofit只是封装,所以不要以为Retrofit是网络请求框架 ...

  3. Excel 多个单元格输入同样内容

    step1: 将这些单元格选定.方法:可以连续选,也可以 ctrl + select不连续选择: step2:输入你想输入的内容,PS:出现在最后选择的单元格中: step3:组合键:ctrl + e ...

  4. WPF ListBox的进阶使用(一)

    公司项目有个需求,UI界面支持动态平均分割界面,想了想便想到用ListBox来实现,用UniformGrid作为ListBox的ItemsPanelTemplate,通过动态改变UniformGrid ...

  5. UWP Button添加圆角阴影(一)

    原文:UWP Button添加圆角阴影(一) 众所周知,17763之前的UWP控件,大部分是没有圆角属性的:而阴影也只有17763中的ThemeShadow可以直接在xaml中使用,之前的版本只能用D ...

  6. 剑指offer编程题Java实现——面试题7相关题用两个队列实现一个栈

    剑指offer面试题7相关题目:用两个队列实现一个栈 解题思路:根据栈的先入后出和队列的先入先出的特点1.在push的时候,把元素向非空的队列内添加2.在pop的时候,把不为空的队列中的size()- ...

  7. Duolingo 提高用户留存率的6个手段

    翻译 :马玉洁 欢迎访问网易云社区,了解更多网易技术产品运营经验. 如果你用过"Duolingo"(Duolingo)这个语言教育应用程序,你就会知道它就像一款游戏. 这当然不是巧 ...

  8. 关于 kali linux

    2.更新系统:首先更换一个速度快点的国内源(1) lsb_release -a先看你的版本,是Rolling还是其他什么(2) leafpad /etc/apt/sources.list(源的默认文件 ...

  9. PHP 获取两个时间之间的月份

    ## 获取两个时间之间的间距时间 $s = '2017-02-05'; $e = '2017-07-20'; $start = new \DateTime($s); $end = new \DateT ...

  10. VNC黑屏解决办法

    在Linux里安装配置完VNC服务端,发现多用户登陆会出现黑屏的情况,具体的现象为:客户端可以通过IP与会话号登陆进入系统,但登陆进去是漆黑一片,除了一个叉形的鼠标以外,伸手不见五指. 原因:用户的V ...