首页 > 技术文章 > Timer 原理 到 spring 定时器任务

cxygg 2022-05-24 21:55 原文

Java Timer 怎么实现延时任务的?怎么实现周期任务的?消耗资源多吗?执行时间准确吗?

 

1 Timer 的基本使用。

  

 

 

  第一个参数是任务,第二个参数可以是指定时间,第三个参数如果指定了就会周期的执行这个任务

 

 

 

2 Timer 的原理

  概述:Timer 有一个内部线程,和一个阻塞队列,在Timer 执行下一个任务之前会wait指定时间,在Timer 里面阻塞队列是空的时候会无限期wait,然后再再新加入任务(2分查找找到插入位置)的以后弹出需要最近执行的一个任务,并且唤醒阻塞的循环消费任务的线程。在循环消费任务的线程中,如果任务是周期任务那么就计算出下一个执行时间点,然后把它插入到按照时间排序的队列中(插入过程使用2分查找),然后判断当前任务是否到了激活时间,如果到了就执行,如果没到就睡到指定时间。

 

 

源码解析:

  1 添加任务以后判断了延时时间不能是负数,然后调用 sched

 

 

 

 

  2 sched 方法,做了一些判断断,然后拿到任务队列的锁,然后阻塞用队列的添加方法,添加完成以后,判断如果当前新加入的任务是排最前面的,那么就唤醒任务线程。

    private void sched(TimerTask task, long time, long period) {
        if (time < 0)
            throw new IllegalArgumentException("Illegal execution time.");

        // Constrain value of period sufficiently to prevent numeric
        // overflow while still being effectively infinitely large.
        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();
        }
    }

  

 3 阻塞队列的添加方法,判断是否需要拓容,通过二分查找下一次执行时间,找应该插入的位置

    void add(TimerTask task) {
        // Grow backing store if necessary
        if (size + 1 == queue.length)
            queue = Arrays.copyOf(queue, 2*queue.length);

        queue[++size] = task;
        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;
        }
    }

  

 4循环任务,循环过程,如果任务队列是空就等着(直到添加元素后唤醒它),醒了 以后取出最上面的任务,获取任务锁,如果任务取消,直接下跳过,比对当前时间和执行时间,判断任务是否激活,没激活就等到需要激活的时间,

  如果激活了,如果是普通任务,直接从队里里面删除,修改执行状态,如果是循环任务,那么计算出下次执行时间,并且在队列里面重排序这个任务,最后调用这个任务 run 方法。

    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
                    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
                                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) {
            }
        }
    }

  

 

 

 

 

 

3 什么情况下Timer  应该避免怎么用

  因为一个Timer 里面有一个 线程,如果我们有多个延时任务,多种不同周期的任务时,我们应该考虑使用少量或者一个TImer,而不是每种甚至每个任务都创建一个Timer,避免大量资源浪费在多线程上下文切换只上。

 

 

4 spring   @Scheduled 的定时任务和 实现了 SchedulingConfigurer 的 定时任务

 

  他们都支持 cron ,这种定时器原理其实类似于Timer 周期执行,在执行当前任务的时候通过 cron 计算出 下一次执行的时间,然后插入到 任务队列的指定位置就行了。使用 SchedulingConfigurer自定义定人任务的时候同样 要避免太过大量的创建 SchedulingConfigurer 的个数,避免太多线程上下文切换带来的消耗。

  

推荐阅读