Java定时器之JDK自带的定时器实现方式

既然要详细说说定时器,就由浅入深,先从最简单的说起。
我首先接触到的定时器就是根据线程的Thread.sleep()方法实现的,最开始学习java的时候,会用这个方法实现一些简单的动画效果,今天就来回顾一下当初的小动画!

1. 利用Thread.sleep();方法实现定时任务

首先 Thread.sleep(times)方法是干嘛的呢,它是用来阻塞当前线程运行的一个方法,按字面意思就是让当前线程睡一会,把CPU资源让给其他线程……你给它传入一个long参数,就是你希望她睡多久的时间值。
随便一提,我旁边的老变态面试新人的时候就喜欢问他们:sleep和wait的区别,请自行百度……
示例代码:

    @Test
    public void threadTimer() throws IOException {
    
    
        // 每一秒钟执行一次
        final long timeInterval = 1000;
        Runnable runnable = new Runnable() {
    
    
            public void run() {
    
    
                while (true) {
    
    
                    // ------- code for task to run
                    // ------- 要运行的任务代码
                    System.out.println("Hello, stranger");
                    // ------- ends here
                    try {
    
    
                        // sleep():同步延迟数据,并且会阻塞线程
                        Thread.sleep(timeInterval);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        };
        //创建定时器
        Thread thread = new Thread(runnable);
        //开始执行
        thread.start();
        //阻止进程结束
        System.in.read();
    }

上面这个代码就是利用Thread.sleep实现一秒间隔的定时任务。之前用这种方式实现过动画效果,但是直接用它来做项目级的定时任务有点傻,这种定时器明显的缺点就是没有计算任务执行本身所花的时间。
由于执行任务的线程只有一个,所以如果某个任务的执行时间过长,那么将破坏其他任务的定时精确性。如一个任务每1秒执行一次,而另一个任务执行一次需要5秒,那么如果是固定速率的任务,那么会在5秒这个任务执行完成后连续执行5次,而固定延迟的任务将丢失4次执行。

2. java.util.Timer

相对于业余的Thread.sleep方法,JDK其实直接就给我们提供了简单好用的定时器–Timer
Timer是用于管理在后台执行的延迟任务或周期性任务,其中的任务使用java.util.TimerTask表示。任务的执行方式有两种:

  • 按固定延迟执行:即schedule的4个重载方法
  • 按固定速率执行:即scheduleAtFixedRate的两个重载方法

分别演示一下:

// 两秒后执行一次任务,且只执行一次
	Timer timer = new Timer();
        timer.schedule(new TimerTask() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("Timer is running");
            }
        }, 2000);

可以看到schedule方法有两个参数,一个是实现了TimerTask的任务参数,另一个就是执行任务的倒计时,一旦倒计时结束,就执行一次任务。
如果想间隔性的执行任务,就要用到scheduleAtFixedRate:

	//表示2秒后开始执行,然后每隔5秒执行一次
	Timer timer = new Timer();
        timer. scheduleAtFixedRate(new TimerTask() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("Timer is running");
            }
        }, 2000, 5000);

Timer是线程安全的,没必要担心线程问题。当然了,Timer的缺点和上面的方式一样,都是单线程执行任务,没有时间间隔的精准度。所以利用线程池来实现定时任务的ScheduledExecutorService 横空出世,因为使用了线程池,提供了良好的约定,以便精准设定执行的时间间隔。我称他为第二代定时器!!

3. ScheduledThreadPoolExecutor

JDK5之后,推荐使用这个类实现定时器。
先写个简单的实例:

		//输入的参数2代表线程池中线程的数量。
        ScheduledThreadPoolExecutor scheduled = new ScheduledThreadPoolExecutor(2);
        scheduled.scheduleAtFixedRate(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.print("time:");
            }
        }, 0, 40, TimeUnit.MILLISECONDS);
        //0表示首次执行任务的延迟时间,40表示每次执行任务的间隔时间,TimeUnit.MILLISECONDS执行的时间间隔数值单位
        //意思就是:0秒后开始执行,然后每40毫秒都会执行一次任务。

其中可设置的时间单位为:
间隔单位毫秒:TimeUnit.MILLISECONDS
间隔单位秒:TimeUnit.SECONDS
间隔单位分钟:TimeUnit.MINUTES
间隔单位小时:TimeUnit.HOURS
间隔单位天:TimeUnit.DAYS

ScheduledThreadPoolExecutor 同样也提供了如同Timer的scheduled方法,用法和timer的一样,执行一次任务,不再演示。

对比ScheduledThreadPoolExecutor和Timer

下面我们做一个小实验,对比一下ScheduledThreadPoolExecutor和timer在执行任务的时候定时误差。
我们分别同timer和ScheduledThreadPoolExecutor写一个定时任务,都设置为2秒后执行,每秒执行一次。
如下:

    static int index;
    @Test
    public void timer() throws IOException
    {
    
    
        final Timer timer = new Timer();
        timer. scheduleAtFixedRate(new TimerTask() {
    
    
            @Override
            public void run() {
    
    
                index++;
                System.out.println("Timer= " + getTimes()+" "  +index);
                try {
    
    
                    //模拟任务执行需要2秒的时间
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                if(index >=10){
    
    
                    timer.cancel();
                    System.out.println("停止了????");
                }
            }
            //2秒后执行,每秒执行一次
        }, 2000, 1000);
        System.in.read();
    }

	static ScheduledThreadPoolExecutor stpe = null;
  @Test
    public void executorTimer() throws IOException
    {
    
    
        // TODO code application logic here
        //构造一个ScheduledThreadPoolExecutor对象,并且设置它的容量为5个
        stpe = new ScheduledThreadPoolExecutor(5);
        //隔2秒后开始执行任务,并且在上一次任务开始后隔1秒再执行一次;
        stpe.scheduleWithFixedDelay(new Runnable()
        {
    
    
            @Override
            public void run()
            {
    
    
                index++;
                System.out.println("executor= " + getTimes()+" "  +index);
                try {
    
    
                    //模拟任务执行需要2秒的时间
                    Thread.currentThread().sleep(2000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                if(index >=10){
    
    
                    stpe.shutdown();
                    if(stpe.isShutdown()){
    
    
                        System.out.println("停止了????");
                    }
                }
            }
        }, 2, 1, TimeUnit.SECONDS);
        //隔6秒后执行一次,但只会执行一次。
//        stpe.schedule(task, 6, TimeUnit.SECONDS);
        System.in.read();
    }

    private static String getTimes() {
    
    
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss E");
        Date date = new Date();
        date.setTime(System.currentTimeMillis());
        return format.format(date);
    }

稍微解释一下,上面有两个Test方法,和一个getTimes(返回当前时间,精确到秒)方法,其中timer是用timer实现定时器,executorTimer是用ScheduledThreadPoolExecutor实现定时器。
两个定时器都是两秒后执行,每秒执行一次,共执行十次,任务都是调用getTimers方法获取并打印当前时间,且会有一秒的线程暂停来模拟执行任务需要一秒钟。
那么我们分别运行一下看一下执行结果:
Timer:

Timer= 2020-12-25 00:09:32 Fri 1
Timer= 2020-12-25 00:09:34 Fri 2
Timer= 2020-12-25 00:09:36 Fri 3
Timer= 2020-12-25 00:09:38 Fri 4
Timer= 2020-12-25 00:09:40 Fri 5
Timer= 2020-12-25 00:09:42 Fri 6
Timer= 2020-12-25 00:09:44 Fri 7
Timer= 2020-12-25 00:09:46 Fri 8
Timer= 2020-12-25 00:09:48 Fri 9
Timer= 2020-12-25 00:09:50 Fri 10
停止了????

ScheduledThreadPoolExecutor:

executor= 2020-12-25 00:16:38 Fri 1
executor= 2020-12-25 00:16:41 Fri 2
executor= 2020-12-25 00:16:44 Fri 3
executor= 2020-12-25 00:16:47 Fri 4
executor= 2020-12-25 00:16:50 Fri 5
executor= 2020-12-25 00:16:53 Fri 6
executor= 2020-12-25 00:16:56 Fri 7
executor= 2020-12-25 00:16:59 Fri 8
executor= 2020-12-25 00:17:02 Fri 9
executor= 2020-12-25 00:17:05 Fri 10
停止了????

可以看出来Timer是用了两秒执行一次,而使用了传说中更稳定的ScheduledThreadPoolExecutor却是3秒执行一次,而两者都是设定的每秒执行一次,所以我的测试结果是ScheduledThreadPoolExecutor误差更大……

我会继续查找资料,找一下这个问题出现的原因,然后更新在此。

-----------------------------------------------------------分割线--------------------------------------------------------------------------
根据进一步的查阅和调试,发现之前做的实验本身有问题:
ScheduledThreadPoolExecutor执行定时任务有三种方法:

  • schedule 创建并执行在给定延迟后启用的单次操作。
  • scheduleAtFixedRate 创建并执行在给定的初始延迟之后,随后以给定的时间段首先启用的周期性动作
  • scheduleWithFixedDelay 创建并执行在给定的初始延迟之后首先启用的定期动作,随后在一个执行的终止和下一个执行的开始之间给定的延迟

我上面实验用的是scheduleWithFixedDelay 方法,该方法是任务执行完之后,再计时间隔时间,然后执行下一个任务。上面设置的任务间隔为1秒,每个任务大约花费2秒,所以两次打印间隔三秒。这就能说的通了,所以使用了线程池,提供了良好的约定,以便精准设定执行的时间间隔的那个定时人无方法,其实应该是ScheduledThreadPoolExecutor的scheduleAtFixedRate 吗?

其实并不是我们想的那样scheduleAtFixedRate能够多线程执行任务,ScheduledThreadPoolExecutor比我想的更有趣,下一篇继续为大家揭秘:详解:被人误解的ScheduledThreadPoolExecutor定时器

猜你喜欢

转载自blog.csdn.net/weixin_41674401/article/details/111657597