java timer并发_Java并发编程笔记之Timer源码分析
发布日期:2021-06-24 15:57:33 浏览次数:2 分类:技术文章

本文共 4188 字,大约阅读时间需要 13 分钟。

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 classTimerTest {//创建定时器对象

static Timer timer = newTimer();public static voidmain(String[] args) {//添加任务1,延迟500ms执行

timer.schedule(newTimerTask() {

@Overridepublic voidrun() {

System.out.println("---one Task---");try{

Thread.sleep(1000);

}catch(InterruptedException e) {

e.printStackTrace();

}throw new RuntimeException("error");

}

},500);//添加任务2,延迟1000ms执行

timer.schedule(newTimerTask() {

@Overridepublic voidrun() {for(;;) {

System.out.println("---two Task---");try{

Thread.sleep(1000);

}catch(InterruptedException e) {//TODO Auto-generated catch block

e.printStackTrace();

}

}

}

},1000);

}

}

如上代码先添加了一个任务在 500ms 后执行,然后添加了第二个任务在 1s 后执行,我们期望的是当第一个任务输出 ---one Task--- 后等待 1s 后第二个任务会输出 ---two Task---,

但是执行完毕代码后输出结果如下所示:

4653dd4b677b41f49e67534e071258bf.png

例子2,

public classShedule {private static longstart;public static voidmain(String[] args) {

TimerTask task= newTimerTask() {public voidrun() {

System.out.println(System.currentTimeMillis()-start);try{

Thread.sleep(3000);

}catch(InterruptedException e){

e.printStackTrace();

}

}

};

TimerTask task1= newTimerTask() {

@Overridepublic voidrun() {

System.out.println(System.currentTimeMillis()-start);

}

};

Timer timer= newTimer();

start=System.currentTimeMillis();//启动一个调度任务,1S钟后执行

timer.schedule(task,1000);//启动一个调度任务,3S钟后执行

timer.schedule(task1,3000);

}

}

上面程序我们预想是第一个任务执行后,第二个任务3S后执行的,即输出一个1000,一个3000.

实际运行结果如下:

39ac4e8ab5d25f5ade3475feb15ea000.png

实际运行结果并不如我们所愿。世界结果,是过了4S后才输出第二个任务,即4001约等于4秒。那部分时间时间到哪里去了呢?那个时间是被我们第一个任务的sleep所占用了。

现在我们在第一个任务中去掉Thread.sleep();这一行代码,运行是否正确了呢?运行结果如下:

7a0260489f6c41fdc3020cb70e2944e7.png

可以看到确实是第一个任务过了1S后执行,第二个任务在第一个任务执行完后过3S执行了。

这就说明了Timer只创建唯一的线程来执行所有Timer任务。如果一个timer任务的执行很耗时,会导致其他TimerTask的时效准确性出问题。

Timer 实现原理分析

下面简单介绍下 Timer 的原理,如下图是 Timer 的原理模型介绍:

14791b3ecf42cd28cb686e67e3d553c7.png

1.其中 TaskQueue 是一个平衡二叉树堆实现的优先级队列,每个 Timer 对象内部有唯一一个 TaskQueue 队列。用户线程调用 timer 的 schedule 方法就是把 TimerTask 任务添加到 TaskQueue 队列,在调用 schedule 的方法时候 long delay 参数用来说明该任务延迟多少时间执行。

2.TimerThread 是具体执行任务的线程,它从 TaskQueue 队列里面获取优先级最小的任务进行执行,需要注意的是只有执行完了当前的任务才会从队列里面获取下一个任务而不管队列里面是否有已经到了设置的 delay 时间,一个 Timer 只有一个 TimerThread 线程,所以可知 Timer 的内部实现是一个多生产者单消费者模型。

从实现模型可以知道要探究上面的问题只需看 TimerThread 的实现就可以了,TimerThread 的 run 方法主要逻辑源码如下:

public voidrun() {try{

mainLoop();

}finally{//有人杀死了这个线程,表现得好像Timer已取消

synchronized(queue) {

newTasksMayBeScheduled= false;

queue.clear();//消除过时的引用

}

}

}private voidmainLoop() {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 classScheduledThreadPoolExecutorTest {static ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);public static voidmain(String[] args) {

scheduledThreadPoolExecutor.schedule(newRunnable() {public voidrun() {

System.out.println("---one Task---");try{

Thread.sleep(1000);

}catch(InterruptedException e) {

e.printStackTrace();

}throw new RuntimeException("error");

}

},500, TimeUnit.MICROSECONDS);

scheduledThreadPoolExecutor.schedule(newRunnable() {public voidrun() {for (int i =0;i<5;++i) {

System.out.println("---two Task---");try{

Thread.sleep(1000);

}catch(InterruptedException e) {

e.printStackTrace();

}

}

}

},1000, TimeUnit.MICROSECONDS);

scheduledThreadPoolExecutor.shutdown();

}

}

运行结果如下:

e7ee88fd56eb95f1754b90149e2eae9f.png

之所以 ScheduledThreadPoolExecutor 的其他任务不受抛出异常的任务的影响是因为 ScheduledThreadPoolExecutor 中的 ScheduledFutureTask 任务中 catch 掉了异常,但是在线程池任务的 run 方法内使用 catch 捕获异常并打印日志是最佳实践。

转载地址:https://blog.csdn.net/weixin_33682804/article/details/114733450 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:java项目事故_记录 Linux环境下java web项目CPU爆表 “事故”,肇事者:GC
下一篇:k近邻算法python解读_机器学习(K-近邻算法)Python的基础知识

发表评论

最新留言

路过按个爪印,很不错,赞一个!
[***.219.124.196]2024年04月10日 19时38分24秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章