Java古老的定时器学习--Timer类

Java在1.3版本引入了Timer工具类,它是一个古老的定时器,搭配TimerTask和TaskQueue一起使用。对于他的实现有许多致命的问题,所以后续在java5又在并发包中引入了又一定时器ScheduledThreadPoolExecutor。

下面来说说Timer的那些缺陷和特点:

1. Timer是一条单线程,它的底层是自己创建了一个线程,用于执行自身任务队列中的任务。

下面来看看源码

    //调用构造方法创建Timer对象时,线程就被启用
    Timer timer = new Timer;

    //....Timer类源码如下:
    //---The Timer thread;
    private final TimerThread thread = new TimerThread(queue);

    //-----Timer的构造方法
    public Timer(String name, boolean isDaemon) {
        var threadReaper = new ThreadReaper(queue, thread);
        this.cleanup = CleanerFactory.cleaner().register(this, threadReaper);
        thread.setName(name);
        thread.setDaemon(isDaemon);
        //启用线程,而对于java启用线程的方式是通过调用start()方法调用Thread类的实例对象中的run()方法
        thread.start();
    }

当线程被调用,执行run()方法

//...Timer的内部类
class TimerThread extends Thread {

    boolean newTasksMayBeScheduled = true;

    //任务队列
    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
            }
        }
    }

接着来看看run()方法下面的mainLoop()方法

    /**
     * The main timer loop.  (See class comment.)
     * 主计时器循环
     */
    private void mainLoop() {
        while (true) {
            try {
                TimerTask task; //任务队列中的任务
                boolean taskFired; //任务是否被触发
                synchronized(queue) {
                    
                    // 任务队列为空时会等待
                    //一旦newTasksMayBeScheduled标志为 true,并且我们的队列中没有更多任务,    
                    //我们就没有工作要做,因此我们优雅地终止。
                    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;  // 继续循环查找任务
                        }

                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        //判断任务是否过期
                        if (taskFired = (executionTime<=currentTime)) { 
                            if (task.period == 0) { // 是否会再次执行
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { //重复任务,重复安排
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    if (!taskFired) //任务未触发,等待
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)  //任务被触发,运行
                    task.run();
            } catch(InterruptedException e) {
            }
        }
    }

这一个方法实际上就是一直循环判断任务队列中任务的状态和是否被触发,以及是否需要重复执行。如果队列中没有任务或者到手的任务还不到触发时机就会将任务队列等待起来,然后再次判断,以此来执行定时器的业务。

而又因为它是一条独立线程的存在,以及它的自身循环机制。可以让它在任务不出错的前提下一直等着任务来。但是这也不可避免的导致如果任务中有一条任务崩掉,那么整个计时器都得挂掉。所以风险很大,并不适用于高并发的场景。

2. 在定时器循环等待任务时,其他线程可以调用它的引用来为队列添加任务。相当于一个一直在工作的机器,我们可以扔东西给他加工,而没有任务时只要不出错,机器也能一直运转等待。当任务添加到队列中后,会唤醒任务队列,也就是唤醒给这个机器丢任务的传送带,这个机器去拉取任务,判断执行与否。

下面是源码:

因为添加任务到队列的方式有很多,这里面我也就只列出来一种,我们只来看看学习它的底层逻辑。

    //创建Timer,添加任务
    Timer t = new Timer;
    t.schedule(...)//此处参数省略了

    //...Timer类
    //参数一:任务,需要是Runnable接口的实现类;
    //参数二:触发时间
    //参数三:任务周期,也就是重复执行的时间间隔。与参数二都是以毫秒为单位
    public void schedule(TimerTask task, long delay, long period) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, System.currentTimeMillis()+delay, -period);
    }

    //sched()方法
    //
    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(); //唤醒任务队列
        }
    }

    //将任务提升到优先级队列
    void add(TimerTask task) {
        //如有必要,增加后备存储
        if (size + 1 == queue.length)
            queue = Arrays.copyOf(queue, 2*queue.length);

        queue[++size] = task;
        fixUp(size);
    }


    //此方法的工作原理是反复“提升”队列 [k](通过将其与其父级交换),直到 queue[k] 的 nextExecutionTime 大于或等于其父级的 nextExecutionTime
    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;
        }
    }

以上就是我个人对这个java的定时器的学习心得,当然其中还有很多是我没有学到的,而我学到的只是展示出来的这一点点。不足之处还请各位大佬指正。

猜你喜欢

转载自blog.csdn.net/Ccc67ol/article/details/128392473