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

(完)