Java Review - 使用Timer时需要注意的事情
概述
先说结论 当一个Timer运行多个TimerTask时,只要其中一个TimerTask在执行中向run方法外抛出了异常,则其他任务也会自动终止。
我们看插件的提示
问题复现
import java.util.Timer;
import java.util.TimerTask;
/**
* @author 小工匠
* @version 1.0
* @description: TODO
* @date 2021/11/21 20:28
* @mark: show me the code , change the world
*/
public class TimerTest {
// 定时器
static Timer timer = new Timer();
public static void main(String[] args) {
// 任务1 , 延迟500ms执行 1秒执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Task1 Running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 模拟发生异常
throw new RuntimeException();
}
},500,1000);
// 任务2, 延迟1000ms执行 1秒执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Task2 Running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},1000,1000);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
如上代码首先添加了第一个任务,让其在500ms后执行。然后添加了第二个任务在1s后执行,我们期望当第一个任务输出Task1 Running后,等待1s,第二个任务输出Task1 Running,,然后循环,每隔1秒执行一次。
但是执行代码后,输出结果为
源码分析
Timer的原理模型如下
-
TaskQueue是一个由平衡二叉树堆实现的优先级队列,每个Timer对象内部有一个TaskQueue队列。用户线程调用Timer的schedule方法就是把TimerTask任务添加到TaskQueue队列。在调用schedule方法时,long delay参数用来指明该任务延迟多少时间执行。
-
·TimerThread是具体执行任务的线程,它从TaskQueue队列里面获取优先级最高的任务进行执行。需要注意的是,只有执行完了当前的任务才会从队列里获取下一个任务,而不管队列里是否有任务已经到了设置的delay时间。一个Timer只有一个TimerThread线程,所以可知Timer的内部实现是一个多生产者-单消费者模型。
源码分析
从该实现模型我们知道,要探究上面的问题只需研究TimerThread的实现就可以了。TimerThread的run方法的主要逻辑代码如下。
/**
* This "helper class" implements the timer's task execution thread, which
* waits for tasks on the timer queue, executions them when they fire,
* reschedules repeating tasks, and removes cancelled tasks and spent
* non-repeating tasks from the queue.
*/
class TimerThread extends Thread {
/**
* This flag is set to false by the reaper to inform us that there
* are no more live references to our Timer object. Once this flag
* is true and there are no more tasks in our queue, there is no
* work left for us to do, so we terminate gracefully. Note that
* this field is protected by queue's monitor!
*/
boolean newTasksMayBeScheduled = true;
/**
* Our Timer's queue. We store this reference in preference to
* a reference to the Timer so the reference graph remains acyclic.
* Otherwise, the Timer would never be garbage-collected and this
* thread would never go away.
*/
private TaskQueue queue;
TimerThread(TaskQueue queue) {
this.queue = queue;
}
public void run() {
try {
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
/**
* The main timer loop. (See class comment.)
*/
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
// 从队列里面获取任务时要加锁
synchronized(queue) {
.........
if (taskFired) // Task fired; run it, holding no locks
task.run();// 执行任务
} catch(InterruptedException e) {
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
当任务在执行过程中抛出InterruptedException之外的异常时,唯一的消费线程就会因为抛出异常而终止,那么队列里的其他待执行的任务就会被清除。
How to Fix
方法一 : run方法内最好使用try-catch结构捕捉可能的异常,不要把异常抛到run方法之外
所以在TimerTask的run方法内最好使用try-catch结构捕捉可能的异常,不要把异常抛到run方法之外。
推荐 ScheduledThreadPoolExecutor
其实要实现Timer功能,使用ScheduledThreadPoolExecutor
的schedule是比较好的选择。如果ScheduledThreadPoolExecutor
中的一个任务抛出异常,其他任务则不受影响。
public class TimerTest {
public static void main(String[] args) throws InterruptedException {
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
scheduledThreadPoolExecutor.schedule(()->{
System.out.println("Task1 Running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 模拟发生异常
throw new RuntimeException();
},1, TimeUnit.SECONDS);
scheduledThreadPoolExecutor.schedule(()->{
System.out.println("Task2 Running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 关闭线程池
scheduledThreadPoolExecutor.shutdown();
},1, TimeUnit.SECONDS);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
之所以ScheduledThreadPoolExecutor的其他任务不受抛出异常的任务的影响,是因为在ScheduledThreadPoolExecutor中的ScheduledFutureTask任务中catch掉了异常 。
小结
ScheduledThreadPoolExecutor是并发包提供的组件,其提供的功能包含但不限于Timer。Timer是固定的多线程生产单线程消费,但是ScheduledThreadPoolExecutor是可以配置的,既可以是多线程生产单线程消费也可以是多线程生产多线程消费,所以在日常开发中使用定时器功能时应该优先使用ScheduledThreadPoolExecutor。
文章来源: artisan.blog.csdn.net,作者:小小工匠,版权归原作者所有,如需转载,请联系作者。
原文链接:artisan.blog.csdn.net/article/details/121459460
- 点赞
- 收藏
- 关注作者
评论(0)