热爱技术,追求卓越
不断求索,精益求精

使用ScheduledExecutorService替代java.util.Timer实现更优雅的定时任务

在前面的一篇文章《java.util.Timer使用详解及注意事项》中讲到了使用java.util.Timer实现定时任务,其中也讲到了使用java.util.Timer存在一些缺陷和不足。从JDK1.5开始,JDK中增加了接口java.util.concurrent.ScheduledExecutorService,我们可以使用ScheduledExecutorService来实现比java.util.Timer更安全更优雅的定时任务。

java.util.Timer的schedule方法未按预期执行

假设有两个定时任务,一个定时任务执行需要花费1000ms,另一个定时任务执行需要花费5000ms,使用java.util.Timer的schedule方法实现,如下:

package cn.lovecto.test;

import java.util.Timer;
import java.util.TimerTask;

public class TimerJob {

    private static Timer timer = new Timer(TimerJob.class.getName(), true);

    private static long JOB_1_TIME = System.currentTimeMillis();
    private static long JOB_2_TIME = System.currentTimeMillis();

    static {
        //每1000ms执行一次
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                doSomething1();
            }
        }, 0, 1000);
        //每5000执行一次
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                doSomething2();
            }
        }, 0, 5000);
    }

    /**
     * 这个方法执行时间大概是1000ms
     */
    private static void doSomething1() {
        long curent = System.currentTimeMillis();
        long period = curent - JOB_1_TIME;
        JOB_1_TIME = curent;
        System.out.println("doSomething1,period=" + period);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 这个方法执行时间大概是5000ms
     */
    private static void doSomething2() {
        long curent = System.currentTimeMillis();
        long period = curent - JOB_2_TIME;
        JOB_2_TIME = curent;
        System.out.println("doSomething2,period=" + period);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //循环查看执行效果
        while (true) {
            Thread.sleep(100);
        }
    }
}

执行打印结果如下:

doSomething1,period=0
doSomething2,period=1000
doSomething1,period=6000
doSomething2,period=6000
doSomething1,period=6000
doSomething2,period=6000
doSomething1,period=6001
doSomething2,period=6002
doSomething1,period=6001
doSomething2,period=6000
doSomething1,period=6000
doSomething2,period=6000

根据运行结果分析,第一个任务的doSomething1执行时间大概1000ms,设定任务的period也是1000ms,但预期的结果两次执行时间平均是6000ms;第二个任务的doSomething2执行时间大概是5000ms,设定任务的period也是5000ms,但预期结果两次执行时间平均也是6000ms。导致这个问题的原因就是java.util.Timer的内部实现只有一个线程,任务队列有多个时,一个任务的执行时间将会影响到其他任务的执行时间的精确性。

java.util.Timer的scheduleAtFixedRate方法未按预期执行

现在我们把上面的代码修改下,把其中schedule的部分修改为scheduleAtFixedRate,其他地方保持原来的逻辑:

static {
    //每1000ms执行一次
    timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            doSomething1();
        }
    }, 0, 1000);
    //每5000执行一次
    timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            doSomething2();
        }
    }, 0, 5000);
}

运行打印结果如下:

doSomething1,period=1
doSomething2,period=1001
doSomething1,period=6001
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething2,period=10001
doSomething1,period=6000
doSomething1,period=1000
doSomething1,period=1001
doSomething1,period=1000
doSomething1,period=1000
doSomething2,period=10001
doSomething1,period=6001
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething2,period=10001

根据运行结果分析,任务一执行一次后,紧接着任务二执行,任务一第二次执行是6秒之后了,紧接着任务一会再执行4次,任务二再执行,任务二的两次执行时间差平均在10秒。虽然scheduleAtFixedRate相比schedule增加了较短任务执行时间的任务的执行机会,但还是影响了各个任务的执行时间精确性。

所以java.util.Timer适用于只有一个任务,或者多个任务执行时间上并没有太大冲突的情况下,所以一般不建议使用java.util.Timer。JDK1.5后我们多了一种更优雅的选择java.util.concurrent.ScheduledExecutorService。

使用ScheduledExecutorService实现定时任务

同样使用上面的例子,我们使用java.util.concurrent.ScheduledExecutorService的方式来实现:

package cn.lovecto.test;

import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorServiceJob {

    private static ScheduledExecutorService scheduledExecutorService = Executors
            .newScheduledThreadPool(2);
    private static long JOB_1_TIME = System.currentTimeMillis();
    private static long JOB_2_TIME = System.currentTimeMillis();

    static {
        // 每1000ms执行一次
        scheduledExecutorService.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                doSomething1();
            }
        }, 0, 1000, TimeUnit.MILLISECONDS);
        // 每5000ms执行一次
        scheduledExecutorService.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                doSomething2();
            }
        }, 0, 5000, TimeUnit.MILLISECONDS);
    }

    /**
     * 这个方法执行时间大概是1000ms
     */
    private static void doSomething1() {
        long curent = System.currentTimeMillis();
        long period = curent - JOB_1_TIME;
        JOB_1_TIME = curent;
        System.out.println("doSomething1,period=" + period);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 这个方法执行时间大概是5000ms
     */
    private static void doSomething2() {
        long curent = System.currentTimeMillis();
        long period = curent - JOB_2_TIME;
        JOB_2_TIME = curent;
        System.out.println("doSomething2,period=" + period);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 循环查看执行效果
        while (true) {
            Thread.sleep(100);
        }
    }
}

此处使用的是scheduleAtFixedRate方法,执行结果如下:

doSomething1,period=4
doSomething2,period=4
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething2,period=5001
doSomething1,period=1001
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething2,period=5000

分析执行结果,基本达到预期。但如果把上面添加任务的地方换成scheduleWithFixedDelay:

static {
    // 每1000ms执行一次
    scheduledExecutorService.scheduleWithFixedDelay(new TimerTask() {
        @Override
        public void run() {
            doSomething1();
        }
    }, 0, 1000, TimeUnit.MILLISECONDS);
    // 每5000ms执行一次
    scheduledExecutorService.scheduleWithFixedDelay(new TimerTask() {
        @Override
        public void run() {
            doSomething2();
        }
    }, 0, 5000, TimeUnit.MILLISECONDS);
}

执行结果如下:

doSomething2,period=2
doSomething1,period=2
doSomething1,period=2001
doSomething1,period=2001
doSomething1,period=2000
doSomething1,period=2001
doSomething2,period=10001
doSomething1,period=2001
doSomething1,period=2001
doSomething1,period=2004
doSomething1,period=2000
doSomething1,period=2001
doSomething2,period=10000

分析运行结果,虽然每个任务的运行频率基本相同,但跟scheduleAtFixedRate的运行结果却有差别。原因是scheduleAtFixedRate是以上一次任务的开始时间为间隔的,并且当任务执行时间大于设置的间隔时间时,真正间隔的时间由任务执行时间为准;而scheduleWithFixedDelay是以上一次任务的结束时间为间隔的。

我们拿任务一的doSomething1举例,由于scheduleAtFixedRate是以两次任务的开始时间为间隔,所以平均每间隔1000ms会打印一次;scheduleWithFixedDelay是以结束时间和下一次的开始时间为间隔的,doSomething1执行时间1000ms,间隔是1000ms,所以平均每隔2000ms会打印一次。

总结

使用java.util.concurrent.ScheduledExecutorService完全能够胜任java.util.Timer所具备的功能,且java.util.concurrent.ScheduledExecutorService的默认实现是线程池,能够弥补java.util.Timer暴露出来的缺陷。所以使用ScheduledExecutorService替代java.util.Timer实现更优雅的定时任务吧。虽然本例没有讲到异常的情况,但务必在被调度的任务上加上异常捕获,因为异常可能会影响任务接下来的执行。

赞(2)
未经允许不得转载:LoveCTO » 使用ScheduledExecutorService替代java.util.Timer实现更优雅的定时任务

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

热爱技术 追求卓越 精益求精