Timer详解_酒醉梦醒的博客-程序员秘密_timer分析

技术标签: schedule  生产者消费者  定时器  并发编程  

timer介绍:

Timer是Josh Bloch在jdk1.3发布的一个新的api,主要用于做定时任务.

timer的使用:

1:schedule(TimerTask task, long delay) 在delay毫秒的延迟后执行task

2:schedule(TimerTask task, Date time) 在指定的time时间执行task

3:schedule(TimerTask task, long delay, long period) 在delay毫秒延迟后按照period的周期循环定时执行task

4:schedule(TimerTask task, Date firstTime, long period)在指定的firstTime时间开始按照period的周期循环定时执行task

5:scheduleAtFixedRate(TimerTask task, long delay, long period) 这个先理解为和3一样,后面会解释二者的区别

6:scheduleAtFixedRate(TimerTask task, Date firstTime, long period)这个先理解为和4一样,后面会解释二者的区别

6的实例:每天24点去执行定时任务

		Timer timer = new Timer();
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.HOUR_OF_DAY,24);
        cal.set(Calendar.MINUTE,0);
        cal.set(Calendar.SECOND,0);
        timer.scheduleAtFixedRate(new TimerTask() {
    
            @Override
            public void run() {
    
                System.out.println("执行任务");
            }
        },cal.getTime(),24*60*60*1000);

timer源码分析:

TimerThread :单线程/消费者/任务线程
TaskQueue :任务队列/优先队列/最小平衡堆/容器
我们写的定时任务是生产者,TimerThread 是消费者,可以看出这是一个单消费者多生产者的模型,而且这个线程还是采取轮询的方式来消费产品,这两个模型决定了Timer的上限。

	/**
     * timer的任务队列,这个队列共享给TimerThread ,timer.schedule...()生产任务,
     * TimerThread 消费任务,在适合的时候执行该任务,过时了则从队列移除
     */
    private final TaskQueue queue = new TaskQueue();
	//消费者线程
    private final TimerThread thread = new TimerThread(queue);

下面看生产者的核心方法,所有生产者最终都会走到这个方法

生产者代码

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) {
    
            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();//唤醒堆顶任务
        }
    }

任务状态

	//This task has not yet been scheduled.
	//处女,任务还没有被调度,默认是这个
	static final int VIRGIN = 0;
	//任务被调度,但是未执行,就是说在队列等待调度
	static final int SCHEDULED   = 1;
	//已经执行了,或者正在执行
	static final int EXECUTED    = 2;
	//任务被取消with a call to TimerTask.cancel
	static final int CANCELLED   = 3;

queue.add(task)代码

	void add(TimerTask task) {
    
        // 容器初始大小128,以2为指数扩容
        if (size + 1 == queue.length)
            queue = Arrays.copyOf(queue, 2*queue.length);
		//size初始为0,可以看出queue的下标0被舍弃掉了,直接从下标1开始入堆
		//这样一来i的左孩子就是2*i了,右孩子是2*i+1.
        queue[++size] = task;
        fixUp(size);//加入堆中后 可能不是最小堆,所以需要对堆做一次fixup调整为最小平衡堆
    }

fixUp(size)代码

 	private void fixUp(int k) {
    
        while (k > 1) {
    
            int j = k >> 1;
            if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
                break;
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }

这段代码的意思其实就是把刚刚加入堆的任务安排到合适的地方去。直接将代码不好讲,还是看个实例:没有学过堆的同学建议花两小时学一下,不然可能听不懂。假设某个时刻堆的任务是下图所示,2,3,4这些数字代表nextExecutionTime(下次执行的时间),数字越小说明任务优先级越高。
2
/ \
3 4
某个时刻来了一个nextExecutionTime=1的任务,此时堆中任务如下图所示。
2(下标1)
/ \
3 4(下标3)
/
1(下标4)
很明显这已经不是一个最小堆了,我们需要把1往上调整。
现在来看代码:
第一次循环
k=4,j=2
if(3<=1)break;这里不成立,但是如果来了一个任务的nextExecutionTime>=3 这里会直接break掉,因为已经是最小堆
交换1和3
2(下标1)
/ \
1 4(下标3)
/
3(下标4)
k=2
第二次循环
k=2 ,j =1
if(2<=1)break;这里不成立
交换1和2
1(下标1)
/ \
2 4(下标3)
/
3(下标4)
k=1退出循环,已经是最小堆
至此生产者的代码就看完了

消费者代码

	//单消费者
	private void mainLoop() {
    
        while (true) {
    //轮询模式
            try {
    
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
    
                    // Wait for queue to become non-empty
                    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
                    //这两个时间及其重要 
                    //scheduleAtFixedRate和schedule的区别就体现在这两个时间和+-period上面
                    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 周期任务
                            	//这里的代码及其经典够味
                            	//schedule的period传的是-period
                            	//scheduleAtFixedRate的period是+period
                            	//如果是schedule调度,下一次执行时间改为
                            	// currentTime-task.period(当前时间-(-period))
                            	//这里看出来schedule是依据当前时间来调度的
                            	//如果是scheduleAtFixedRate调度,下一次执行时间是
                            	//executionTime + task.period,
                            	//这里看出来scheduleAtFixedRate是依据执行时间调度的
                            	//(这个执行时间是我们写代码指定的那个时间)							
                            	//并且while(true){}保证scheduleAtFixedRate这种调度方式会自动补上之前缺失的任务。
                                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) {
    
            }
        }
    }
}

queue.removeMin();代码

	void removeMin() {
    
        queue[1] = queue[size];//堆顶置为堆尾
        queue[size--] = null;  //堆尾置为null,size--
        fixDown(1);//对堆顶进行一次调整,和fixup反着来,目的都是为了调整成最小堆
    }

timer的schedule和scheduleAtFixedRate区别:

		Timer timer = new Timer();
        SimpleDateFormat fTime = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        Date date = fTime.parse("2019/7/3 10:50:00");
        timer.scheduleAtFixedRate(new TimerTask(){
    
            public void run()
            {
    
                System.out.println("exec task");
            }
        },date,3*60*1000);

程序指定运行时间是2019/7/3 10:50:00,每隔三分钟运行一次
如果我等到2019/7/3 10:55:00 去运行这段程序,即已经过了五分钟了。

使用scheduleAtFixedRate会快速打印两个exec task(第一次2019/7/3 10:50:00,第二次2019/7/3 10:53:00),然后按照2019/7/3 10:56:00–> 2019/7/3 10:59:00这样打印下去。也就是说scheduleAtFixedRate是按照指定的时间开始算,如果程序运行的时间晚于这个指定时间,他会一次性补上之前的任务,然后按照间隔时间去执行。

如果使用schedule他不会补上之前的任务,而且他是按照实际执行程序的时间开始算,也就是说如果2019/7/3 10:55:00用 去schedule运行这段程序那么下一次打印时间将是2019/7/3 10:58:00.

timer的缺点:

缺点一:对于耗时任务及多任务非常不友好

	Timer timer = new Timer();
	final long start = System.currentTimeMillis();
        timer.schedule(new TimerTask() {
    
            @Override
            public void run() {
    
                System.out.println("time1:"+ (System.currentTimeMillis()-start));
                try {
    
                    //模拟耗时操作
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
    
                    e.printStackTrace();
                }
            }
        },1000);

        timer.schedule(new TimerTask() {
    
            @Override
            public void run() {
    
                System.out.println("time2:"+ (System.currentTimeMillis()-start));
            }
        },2000);

我希望的结果是
time1:1001
time2:2001
可实际上由于单线程的原因结果是
time1:1001
time2:4001
由这个例子看出Timer只适用于耗时短的单任务。

缺点二:对于运行时异常不友好

Timer timer = new Timer();
	final long start = System.currentTimeMillis();
        timer.schedule(new TimerTask() {
    
            @Override
            public void run() {
    
                System.out.println("time1:"+ (System.currentTimeMillis()-start));
                throw new RuntimeException();
            }
        },1000);

        timer.schedule(new TimerTask() {
    
            @Override
            public void run() {
    
                System.out.println("time2:"+ (System.currentTimeMillis()-start));
            }
        },2000);

在timer1里抛运行时异常会导致time2不可用,这一点问题不大,我们有最佳实践:在run里面手动catch异常进行处理。

timer的替代产品:

相比于timer,在jdk1.5的时候,Doug Lea老先生主笔写了juc新api,线程池。其中的带有调度功能的线程池就可以执行定时任务,而且性能及稳定性更优秀。线程池将在下篇博文讲到。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/LiuRenyou/article/details/94552916

智能推荐

启动Hadoop2.6 DataNode启动不了的问题_dadammd353690383的博客-程序员秘密

异常信息2018-04-11 22:40:22,786 WARN org.apache.hadoop.hdfs.server.common.Storage: java.io.IOException: Incompatible clusterIDs in /usr/local/src/Hadoop2.0/hadoop/tmp/dfs/data: namenode clusterID = CID-60...

TNS-12514 & TNS-12505 & service_died_cti27515的博客-程序员秘密

客户的一套库,早上突然连不进数据库,报TNS-12514客户自己重启监听后,过了一段时间还是连不上,尝试register,还是报TNS-12505。电话支持,受客户误导,让其测试在服务器上用非SYS用户能否连接数据库,...

Android开发实战讲解!Android开发者跳槽面试,真香!_android loading_Android女王的博客-程序员秘密

目前情况:10届某民办大学本科生,实际接触Android年限6年多了,工作年限五年半(注意,我说的是工作年限,不是工作经验),今年1月份裸辞后歇了大半年,经常一周也收不到几个offer,好不容易熬到HR面,也因为薪资要求过高被放弃了,最终拿到一个并不是特满意的offer。首先我想明确地说在目前的大环境下,移动互联网确实已经属于寒冬。尤其是Android/IOS开发,虽然说不上夕阳行业,但也离热门IT职业差了十万八千里。从之前大量小创公司因疫情原因倒闭破产,360、滴滴、携程等大厂实施裁员的新闻其实也能.

程序员的平方英尺园艺_cunfuteng7334的博客-程序员秘密

I'm not handy, but I'm trying. I want to also point out that I know exactly ZERO about gardening. All that said, here's what I did this year. 我不方便,但我正在尝试。 我还要指出,我对园艺完全了解为零。 这么说,这就是我今年所做的。 4月10日(Apr...

你真正了解@Resource和@Autowired这两个注解吗_kingmax54212008的博客-程序员秘密

你真正了解@Resource和@Autowired这两个注解吗你真正了解@Resource和@Autowired这两个注解吗我们一起来探讨这两个注解以及使用不当引发的案例大部分人都知道@Resource是通过“beanName”注入,@Autowired是通过类型注入,但是真的只有这么简单吗?先抛问题项目中采用spring+mybatis框架,同时引入了zebra(https...

python列表,元组的操作_weixin_30273931的博客-程序员秘密

python的列表是用于存放一组数据,表示形式 用 [ ]来表示1,列表的常用方法:names = ['!alex', 'eric', 'rain', '2kity']names.append('knochkapoor') #增加一个元素names.count('alex') #统计alex数量names.insert(0,'jlliu') #在第一个位置插入'jull...

随便推点

html5支持4所有的表单控件吗,web day02 表格 表单及HTML常用的表单控件_挖数的博客-程序员秘密

一、 表格的标签及属性table属性Width、height 宽高(单位是像素或百分比)align 对齐border 外边框bgcolor 背景色background 背景图片Cellspacing 单元格间距(单元格和单元格的距离) 一般情况写0Cellpadding 单元格边距(表格边框与内容的距离) 一般情况写0tr属性:Align 水平对齐left/center/ rightvalign ...

git 删除未跟踪文件_larance的博客-程序员秘密

# 删除 untracked filesgit clean -f # 连 untracked 的目录也一起删掉git clean -fd # 连 gitignore 的untrack 文件/目录也一起删掉 (慎用,一般这个是用来删掉编译出来的 .o之类的文件用的)git clean -xfd # 在用上述 git clean 

Python学习笔记--8.2 函数--默认值参数_weixin_30823001的博客-程序员秘密

函数的格式:def my(name,sex): pass#函数体。pass表示先占个位置。 name='python' #函数里定义的变量是局部变量 return name #调用函数返回一个返回值其中def my(name,sex):中的name,sex是形式参数(形参)。调用函数:my(xiaoming,nan):其中xiaomi...

Java中Cloneable接口的浅复制与深复制_慵懒的树獭的博客-程序员秘密

Java中的深拷贝(深复制)和浅拷贝(浅复制) 深拷贝(深复制)和浅拷贝(浅复制)是两个比较通用的概念,尤其在C++语言中,若不弄懂,则会在delete的时候出问题,但是我们在这幸好用的是Java。虽然java自动管理对象的回收,但对于深拷贝(深复制)和浅拷贝(浅复制),我们还是要给予...

数据结构+算法--求单链表中有效节点的个数_Hello_World&Java的博客-程序员秘密

求单链表中有效节点的个数思路代码实现(代码中的getLeng方法)思路直接遍历单链表,详情请看代码实现代码实现(代码中的getLeng方法)import java.util.Scanner;public class SingleLinkedListDemo { public static void main(String[] args) { SingleLinkedList singleLinkedList = new SingleLinkedList();

编程老司机带你玩转 CompletableFuture 异步编程_JAVA葵花宝典的博客-程序员秘密

本文从实例出发,介绍 CompletableFuture 基本用法。不过讲的再多,不如亲自上手练习一下。所以建议各位小伙伴看完,上机练习一把,快速掌握 CompletableFuture...