Java 中的 Timer 源码分析及缺陷

使用Timer执行定时任务很简单,一般这样子写:

        Timer timer = new Timer();
        TimerTask task = new TimerTask() {

            @Override
            public void run() {
                System.out.println("hello world");
            }
        };
        timer.schedule(task, 10, 2000);

以上代码创建了一个定时器和定时任务,大部分情况下它都能正常工作,延迟10ms后,每隔2s就打印一个hello world。

但其实Timer是有一些问题的,程序中如果不注意就可能出现问题,下面来自《Java并发编程实战》:

1.Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间长度大于其周期时间长度,那么就会导致这一次的任务还在执行,而下一个周期的任务已经需要开始执行了,当然在一个线程内这两个任务只能顺序执行,有两种情况:对于之前需要执行但还没有执行的任务,一是当前任务执行完马上执行那些任务(按顺序来),二是干脆把那些任务丢掉,不去执行它们。至于具体采取哪种做法,需要看是调用schedule还是scheduleAtFixedRate。
2.如果TimerTask抛出了一个未检出的异常,那么Timer线程就会被终止掉,之前已经被调度但尚未执行的TimerTask就不会再执行了,新的任务也不能被调度了。

下面通过分析Timer的源码来解释为什么会有上面的问题。
Timer的实现原理很简单,概括的说就是:Timer有两个内部类,TaskQueue和TimerThread,TaskQueue其实就是一个最小堆(按TimerTask下一个任务执行时间点先后排序),它存放该Timer的所有TimerTask,而TimerThread就是Timer新开的检查兼执行线程,在run中用一个死循环不断检查是否有任务需要开始执行了,有就执行它(注意还是在这个线程执行)。

因此Timer实现的关键就是调度方法,也就是TimerThread的run方法:

    public void run() {
        try {
            mainLoop();
        } finally {
            
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  
            }
        }
    }

具体逻辑在mainLoop方法中实现:

private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; 

                    
                    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);="" (!taskfired)="" queue.wait(executiontime="" currenttime);="" (taskfired)="" task.run();="" catch(interruptedexception="" e)="" }<="" code="">
    

从第14行开始,这里取出那个最先需要执行的TimerTask,然后22行判断executionTime<=currenttime,其中executiontime就是该timertask下一个周期任务执行的时间点,currenttime为当前时间点,如果为true说明该任务需要执行了(注意可能是一个过时任务,应该在过去某个时间点开始执行,但由于某种原因还没有执行),接着第23行判断task.period ==”0,Timer中period默认为0表示该TimerTask只会执行一次,不会周期性地不断执行,所以为true那么就移除掉该TimerTask,然后待会会执行该TimerTask一次。如果task.period不为0,那就分为小于0和大于0,如果调用的是schedule方法:

    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);="" }<="" code="">
    

那么period就小于0,如果调用的是scheduleAtFixedRate方法:

    public void scheduleAtFixedRate(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);="" }<="" code="">
    

那么period就大于0。
回到mainLoop方法中,当period<0时,当前timertask下一次开始执行任务的时间就会被设置为currenttime -=”” task.period,可理解为定时任务被重置,从现在开始,period周期间隔(那么之前预想在这个间隔内存在的任务执行就没了)后执行第一次任务,这种情况就是timer的任务可能丢失问题。当period=””>0,当前TimerTask下一次开始执行任务的时间就会被设置为executionTime + task.period,即下一次任务还是按原来的算,因此如果这时executionTime + task.period还先于currentTime,那么下一个任务就会马上执行,也就是Timer的任务快速调用问题。

以上分析解释了第一点,下面解释第二点。
从代码上可以看到在死循环中只catch了一个InterruptedException,也就是当前线程被中断,因此Timer的线程是可以执行一段时间,然后被操作系统挂到一边休息,然后又回来继续执行的。但如果抛出其它异常,那么整个循环就挂掉,当然外层的run方法也没有catch任何异常:

    public void run() {
        try {
            mainLoop();
        } finally {
            
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  
            }
        }
    }

这时就会造成线程泄露,同时之前已经被调度但尚未执行的TimerTask就不会再执行了,新的任务也不能被调度了。

补充:对于上面说到的Timer线程执行到一半被挂到一边去,这种情况与任务执行时间过长类似,如果调用schedule方法的话就有可能导致任务丢失。在Android中,有一种叫长连接的东西,它需要客户端发心跳包确保连接的存在,如果使用Timer实现定时发心跳包就可能会有问题,如果Timer线程在执行过程中被换出去了,那么调用schedule的就很有可能导致心跳包没有发出去,而调用scheduleAtFixedRate又可能会导致Timer线程没有占用CPU时心跳包没发出去,某一时刻又快速地发送好几个心跳包。因此在Android中一般使用AlarmManager实现心跳包的定时发送。

从JDK5开始引入了ThreadPoolExecutor,它可以用来实现定时任务,因此一般建议使用它来实现定时任务:

        ScheduledExecutorService ses = Executors.newScheduledThreadPool(5)
        ses.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                System.out.println("hello world")
            }
        }, 10, 2000, TimeUnit.MILLISECONDS)

    原文作者:Android源码分析
    原文地址: https://juejin.im/entry/57ea220bd2030900695956dd
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞