以前在使用Timer作为定时任务时,如果同时执行多个爬虫,且执行的时间过长,超过了两个爬虫的间隔时间,就会出现一些奇怪的错误,在查找了一些资料之后,这次来尝试下ScheduledExecutorService做为定时器,下面是简单的介绍。


java中要实现定时执行的方法通常有三种(这里不包括spring和一些已经实现好的工具类):

  • 第一种就是创建一个线程然后一直循环通过sleep来达到定时执行,但是不容易控制任务的启动和取消
  • 第二种就是使用Timer和TimerTask,这个使用起来相比第一种好了很多,但是做为单线程在处理多个任务和抛出异常的时候就不那么方便了
  • 第三种ScheduledExecutorService是通常最理想的定时任务实现方式,主要将定时任务和线程池功能结合,能够更好的处理java中的一些并发的任务需要。

ScheduledExecutorService,是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。需要注意,只有当调度任务来的时候,ScheduledExecutorService才会真正启动一个线程,其余时间ScheduledExecutorService都是出于轮询任务的状态。

首先是一个简单的例子:

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

public class Test{  
    public static void main(String[] args) {  
        Runnable runnable = new Runnable() {  

        //所要定时执行的任务
            public void run() { 
                System.out.println("Hello !!");  
            }  
        };  
        ScheduledExecutorService service = Executors  
                .newSingleThreadScheduledExecutor();  
        // 第二个参数为首次执行的延时时间,第三个参数为定时执行的间隔时间(下面有解释这两种方法的区别)  
        service.scheduleAtFixedRate(runnable, 10, 1, TimeUnit.SECONDS);
        servuce.scheduleWithFixedDelay(runnable, 10, 1,TimeUnit.SECONDS);  
    }  
}

这里创建任务是使用Runnable接口,在程序开发中只要是多线程肯定永远以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下好处:

  • 避免点继承的局限,一个类可以继承多个接口。
  • 适合于资源的共享

方法介绍:

其中scheduleAtFixedRate(TimerTask task,long delay,long period) 方法用于安排指定的任务进行重复的固定速率执行,在指定的延迟后开始。参数说明如下:

  • task:这是要被调度的任务
  • delay:这是以毫秒为单位的延迟之后的任务执行
  • period:这是在连续执行任务之间的毫秒的时间

在执行这个方法的时候会有两个异常要考虑:

  • IllegalArgumentException–这个异常被抛出,如果time.getTime()为负。
  • IllegalStateException–这将被抛出,如果任务已经安排或取消,计时器被取消,或者计时器线程已终止。

两种方法的区别:

ScheduledExecutorService 中两种最常用的调度方法 ScheduleAtFixedRate 和 ScheduleWithFixedDelay。

  • ScheduleAtFixedRate 每次执行时间为上一次任务开始起向后推一个时间间隔,即每次执行时间为 :initialDelay,
    initialDelay+period, initialDelay+2*period, …;
  • ScheduleWithFixedDelay每次执行时间为上一次任务结束起向后推一个时间间隔,即每次执行时间为:initialDelay,
    initialDelay+executeTime+delay, initialDelay+2*executeTime+2*delay。

由此可见,ScheduleAtFixedRate 是基于固定时间间隔进行任务调度,ScheduleWithFixedDelay 取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度


程序有时候要求任务在每天的某一个时刻运行,下面是我自己写的一个列子,供参考:

import java.text.ParseException;
import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Created by gray- on 2017/10/25.
 *
 *
 * 测试java中并发的ScheduledExecutorService 的定时效果
 *
 *
 * 它具有以下的好处:
 * 1、相比于Timer的单线程,它是通过线程池的方式来执行任务的
 * 2、可以很灵活的去设定第一次执行任务delay的时间
 * 3、提供了更好的约定,以便设定执行的时间间隔
 */
public class ScheduledExecutorServiceTest {





    public static void main(String[] args){
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("hello!!!");
            }
        };


        //在每天的下午五点半运行
        long oneDay = 60 * 60 * 24 * 1000;
        long initDelay = getTimeMillis("17:30:00") - System.currentTimeMillis();

        initDelay = initDelay>0? initDelay : oneDay + initDelay;

        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();

        service.scheduleAtFixedRate(runnable, initDelay , oneDay, TimeUnit.MILLISECONDS);

    }

    public static long getTimeMillis(String time){

        DateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm:ss");

        DateFormat dayFormat = new SimpleDateFormat("yy-MM-dd");

        try {
            Date curDate = dateFormat.parse(dayFormat.format(new Date()) + " " + time);
            return curDate.getTime();
        } catch (ParseException e) {
            e.printStackTrace();
        }

        return 0;

    }


}

以上就是对ScheduledExecutorService的简单的了解