Java并发编程笔记之Timer源码分析
timer在JDK里面,是很早的一个API了。具有延时的,并具有周期性的任务,在newScheduledThreadPool出来之前我们一般会用Timer和TimerTask来做,但是Timer存在一些缺陷,为什么这么说呢?
Timer只创建唯一的线程来执行所有Timer任务。如果一个timer任务的执行很耗时,会导致其他TimerTask的时效准确性出问题。例如一个TimerTask每10秒执行一次,而另外一个TimerTask每40ms执行一次,重复出现的任务会在后来的任务完成后快速连续的被调用4次,要么完全“丢失”4次调用。Timer的另外一个问题在于,如果TimerTask抛出未检查的异常会终止timer线程。这种情况下,Timer也不会重新回复线程的执行了;它错误的认为整个Timer都被取消了。此时已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了。
这里做了一个小的 demo 来复现问题,代码如下:
package com.hjc; import java.util.Timer;
import java.util.TimerTask; /**
* Created by cong on 2018/7/12.
*/
public class TimerTest {
//创建定时器对象
static Timer timer = new Timer(); public static void main(String[] args) {
//添加任务1,延迟500ms执行
timer.schedule(new TimerTask() { @Override
public void run() {
System.out.println("---one Task---");
try {
Thread.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
throw new RuntimeException("error ");
}
}, );
//添加任务2,延迟1000ms执行
timer.schedule(new TimerTask() { @Override
public void run() {
for (;;) {
System.out.println("---two Task---");
try {
Thread.sleep();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}, ); }
}
如上代码先添加了一个任务在 500ms 后执行,然后添加了第二个任务在 1s 后执行,我们期望的是当第一个任务输出 ---one Task--- 后等待 1s 后第二个任务会输出 ---two Task---,
但是执行完毕代码后输出结果如下所示:

例子2,
public class Shedule {
private static long start;
public static void main(String[] args) {
TimerTask task = new TimerTask() {
public void run() {
System.out.println(System.currentTimeMillis()-start);
try{
Thread.sleep();
}catch (InterruptedException e){
e.printStackTrace();
}
}
};
TimerTask task1 = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis()-start);
}
};
Timer timer = new Timer();
start = System.currentTimeMillis();
//启动一个调度任务,1S钟后执行
timer.schedule(task,);
//启动一个调度任务,3S钟后执行
timer.schedule(task1,);
}
}
上面程序我们预想是第一个任务执行后,第二个任务3S后执行的,即输出一个1000,一个3000.
实际运行结果如下:

实际运行结果并不如我们所愿。世界结果,是过了4S后才输出第二个任务,即4001约等于4秒。那部分时间时间到哪里去了呢?那个时间是被我们第一个任务的sleep所占用了。
现在我们在第一个任务中去掉Thread.sleep();这一行代码,运行是否正确了呢?运行结果如下:

可以看到确实是第一个任务过了1S后执行,第二个任务在第一个任务执行完后过3S执行了。
这就说明了Timer只创建唯一的线程来执行所有Timer任务。如果一个timer任务的执行很耗时,会导致其他TimerTask的时效准确性出问题。
Timer 实现原理分析
下面简单介绍下 Timer 的原理,如下图是 Timer 的原理模型介绍:

1.其中 TaskQueue 是一个平衡二叉树堆实现的优先级队列,每个 Timer 对象内部有唯一一个 TaskQueue 队列。用户线程调用 timer 的 schedule 方法就是把 TimerTask 任务添加到 TaskQueue 队列,在调用 schedule 的方法时候 long delay 参数用来说明该任务延迟多少时间执行。
2.TimerThread 是具体执行任务的线程,它从 TaskQueue 队列里面获取优先级最小的任务进行执行,需要注意的是只有执行完了当前的任务才会从队列里面获取下一个任务而不管队列里面是否有已经到了设置的 delay 时间,一个 Timer 只有一个 TimerThread 线程,所以可知 Timer 的内部实现是一个多生产者单消费者模型。
从实现模型可以知道要探究上面的问题只需看 TimerThread 的实现就可以了,TimerThread 的 run 方法主要逻辑源码如下:
public void run() {
try {
mainLoop();
} finally {
// 有人杀死了这个线程,表现得好像Timer已取消
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // 消除过时的引用
}
}
}
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
//从队列里面获取任务时候要加锁
synchronized(queue) {
......
}
if (taskFired)
task.run();//执行任务
} catch(InterruptedException e) {
}
}
}
可知当任务执行过程中抛出了除 InterruptedException 之外的异常后,唯一的消费线程就会因为抛出异常而终止,那么队列里面的其他待执行的任务就会被清除。所以 TimerTask 的 run 方法内最好使用 try-catch 结构 catch 主可能的异常,不要把异常抛出到 run 方法外。
其实要实现类似 Timer 的功能使用 ScheduledThreadPoolExecutor 的 schedule 是比较好的选择。ScheduledThreadPoolExecutor 中的一个任务抛出了异常,其他任务不受影响的。
ScheduledThreadPoolExecutor 例子如下:
/**
* Created by cong on 2018/7/12.
*/
public class ScheduledThreadPoolExecutorTest {
static ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(); public static void main(String[] args) { scheduledThreadPoolExecutor.schedule(new Runnable() { public void run() {
System.out.println("---one Task---");
try {
Thread.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
throw new RuntimeException("error ");
} }, , TimeUnit.MICROSECONDS); scheduledThreadPoolExecutor.schedule(new Runnable() { public void run() {
for (int i =;i<;++i) {
System.out.println("---two Task---");
try {
Thread.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
} } }, , TimeUnit.MICROSECONDS); scheduledThreadPoolExecutor.shutdown();
}
}
运行结果如下:

之所以 ScheduledThreadPoolExecutor 的其他任务不受抛出异常的任务的影响是因为 ScheduledThreadPoolExecutor 中的 ScheduledFutureTask 任务中 catch 掉了异常,但是在线程池任务的 run 方法内使用 catch 捕获异常并打印日志是最佳实践。
Java并发编程笔记之Timer源码分析的更多相关文章
- Java并发编程笔记之CopyOnWriteArrayList源码分析
并发包中并发List只有CopyOnWriteArrayList这一个,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行修改操作和元素迭代操作都是在底层创建一个拷贝 ...
- Java并发编程笔记之ThreadLocalRandom源码分析
JDK 并发包中 ThreadLocalRandom 类原理剖析,经常使用的随机数生成器 Random 类的原理是什么?及其局限性是什么?ThreadLocalRandom 是如何利用 ThreadL ...
- Java并发编程笔记之ThreadLocal源码分析
多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的,多线程访问同一个共享变量特别容易出现并发问题,特别是多个线程需要对一个共享变量进行写入时候, ...
- Java并发编程笔记之FutureTask源码分析
FutureTask可用于异步获取执行结果或取消执行任务的场景.通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过Fu ...
- Java并发编程笔记之SimpleDateFormat源码分析
SimpleDateFormat 是 Java 提供的一个格式化和解析日期的工具类,日常开发中应该经常会用到,但是由于它是线程不安全的,多线程公用一个 SimpleDateFormat 实例对日期进行 ...
- Java并发编程笔记之CyclicBarrier源码分析
JUC 中 回环屏障 CyclicBarrier 的使用与分析,它也可以实现像 CountDownLatch 一样让一组线程全部到达一个状态后再全部同时执行,但是 CyclicBarrier 可以被复 ...
- Java并发编程笔记之PriorityBlockingQueue源码分析
JDK 中无界优先级队列PriorityBlockingQueue 内部使用堆算法保证每次出队都是优先级最高的元素,元素入队时候是如何建堆的,元素出队后如何调整堆的平衡的? PriorityBlock ...
- Java并发编程笔记之ArrayBlockingQueue源码分析
JDK 中基于数组的阻塞队列 ArrayBlockingQueue 原理剖析,ArrayBlockingQueue 内部如何基于一把独占锁以及对应的两个条件变量实现出入队操作的线程安全? 首先我们先大 ...
- Java并发编程笔记之ReentrantLock源码分析
ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞后放入该锁的AQS阻塞队列里面. 首先我们先看一下ReentrantLock的类图结构,如下图所示 ...
随机推荐
- shell脚本语法学习
以下内容仅为个人学习使用,如有错误,欢迎指出 持续更新............... 一.首先创建.sh文件,打开并在文件的第一行输入#!/bin/sh 执行shell文件命令为: . filenam ...
- fly插件飞向购物车
首先载入jQuery库文件和jquery.fly.min.js插件. 插件官方: https://github.com/amibug/fly, 官方例子: http://codepen.io/hzxs ...
- Linux运维40道精华题
题目 1.什么是运维?什么是游戏运维? 1)运维是指大型组织已经建立好的网络软硬件的维护,就是要保证业务的上线与运作的正常,在他运转的过程中,对他进行维护,他集合了网络.系统.数据库.开发.安全.监控 ...
- springBoot基础2
主要记录上一篇 springBoot基础 中涉及到的pom.项目结构.注解等 首先是pom: 关于pom中这段插件配置: <plugin> <groupId>org.sprin ...
- Alpha冲刺 - (7/10)
Part.1 开篇 队名:彳艮彳亍团队组长博客:戳我进入作业博客:班级博客本次作业的链接 Part.2 成员汇报 组员1(组长)柯奇豪 过去两天完成了哪些任务 改用更易用的springboot+myb ...
- uniGUI动态建立Form及释放
uniGUI动态建立Form及释放 (2015-10-01 14:51:12) 转载▼ 分类: uniGUI 用uniGUI开发的项目中,难免要遇到动态建立一个Form,再释放掉,与传统Delph ...
- iOS逆向之iOSOpenDev
上篇谈到使用TheOS进行越狱开发,但是流程相对而言较复杂,本篇我们谈一下iOSOpenDev进行越狱开发.通过使用iOSOpenDev,我们可以使用Xcode进行开发.编译.生成并运行到设备上. 1 ...
- 使用netstat命令查看端口的使用情况
Windows如何查看端口占用情况操作步骤如下: 开始--运行--cmd 进入命令提示符,输入netstat -ano 即可看到所有连接的PID 之后在任务管理器中找到这个PID所对应的程序如果任务管 ...
- 583. Delete Operation for Two Strings
Given two words word1 and word2, find the minimum number of steps required to make word1 and word2 t ...
- 快速部署Apache服务静态网站
Apache是世界使用排名第一的Web服务器软件.它可以运行在几乎所有广泛使用的计算机平台上,由于其跨平台和安全性被广泛使用,是最流行的Web服务器端软件之一.它快速.可靠并且可通过简单的API扩充, ...