httpclient导致的spring schedule定时任务全部停止问题

前两天vos线上服务遇到一个问题,定时任务突然全部停下来不跑了。看日志也没发现什么明显的异常输出,加上比较忙,所以干脆直接使用重启大法。没想到过了两天突然发现定时任务又全部停了,恰好正在赶需求,vos又是一个公司内部使用的系统,优先级较低,索性继续重启等有空了再来看。然后过了1天多定时任务㕛挂了。只能暂时放下手头上的事看一下到底是什么导致的这个问题。

由于日志未输出任何异常信息,所以要解决该问题只能靠分析代码了。由于vos系统使用的定时任务框架是spring schduler,所以打算了解一下spring schduler的运行原理。

一、spring schduler 的运行原理

一般我们要使用schuduler,都会往spring的配置文件中加入下面这行。

<task:annotation-driven />

然后通过xml或者注解@Scheduled的方式配置具体的定时任务。下面我们围绕注解的方式讲解一下spring schduler是怎么解析和运行配置的定时任务的。

1. 配置解析

spirng在解析配置文件的时候遇到的时候,会往spring容器中注入 ScheduledAnnotationBeanPostProcessor 对象实例。具体的解析过程这里就不详述了。感兴趣的可以自行百度或者看一下下面这个链接。
http://blog.csdn.net/tt50335971/article/details/52055755

2. ScheduledAnnotationBeanPostProcessor类

ScheduledAnnotationBeanPostProcessor 是BeanPostProcessor的子类。在spring bean的生命周期中,所有的bean构造完后都会执行postProcessAfterInitialization()方法。因此,spring容器注入ScheduledAnnotationBeanPostProcessor对象后,每次容器构造bean后都会执行ScheduledAnnotationBeanPostProcessor#postProcessAfterInitialization()方法。下面我们来看下这个方法大体长什么样:

public Object postProcessAfterInitialization(final Object bean, String beanName) {
                //获取具体的类
        final Class<?> targetClass = AopUtils.getTargetClass(bean);
                //获取并遍历该类的所有方法
        ReflectionUtils.doWithMethods(targetClass, new MethodCallback() {
            public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
                                 //获取该方法的Scheduled注解
                Scheduled annotation = AnnotationUtils.getAnnotation(method, Scheduled.class);
                                 //如果方法没有Scheduled注解,那就没有必要执行了
                if (annotation != null) {
                    try {
                        ///省略一些代码
                        //获取Scheduled注解的cron属性值
                        String cron = annotation.cron();
                        if (!"".equals(cron)) {
                            Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
                            processedSchedule = true;
                            if (embeddedValueResolver != null) {
                                cron = embeddedValueResolver.resolveStringValue(cron);
                            }
                                                        //往registrar加一个定时任务
                            registrar.addCronTask(new CronTask(runnable, cron));
                        }
                        // 下面代码省略,具体读者可以自行打开IDE查看这个类
                }
            }
        });

3. ScheduledTaskRegistrar类

从上面可以看出,ScheduledAnnotationBeanPostProcessor会遍历spring 容器中所有bean的所有方法,如果发现有Scheduled注解的,就解析然后往registrar加入一个CronTask对象。所以我们再接着来分析registrar。从代码可以很直观看到,它对应的类是ScheduledTaskRegistrar。我们来看一下它一些变量的定义

    private TaskScheduler taskScheduler;
    private ScheduledExecutorService localExecutor;
    private List<TriggerTask> triggerTasks;
    private List<CronTask> cronTasks;
    private List<IntervalTask> fixedRateTasks;
    private List<IntervalTask> fixedDelayTasks;
    private final Set<ScheduledFuture<?>> scheduledFutures = new LinkedHashSet<ScheduledFuture<?>>();

在前面的代码中,spring将各个Task加入到各种List中。但是各种Task只是一个抽象的类,java程序并不会识别他们并定时的执行任务。那是什么时候讲Task转化成真正运行着的定时任务呢?
这里我们先来看下ScheduledTaskRegistrar的初始化。由于它实现了InitializingBean接口,所以它构造完后会执行对应的afterPropertiesSet方法。

public void afterPropertiesSet() {
        scheduleTasks();
}
protected void scheduleTasks() {
        long now = System.currentTimeMillis();

        if (this.taskScheduler == null) {
            //构造一个单线程的线程池。所以所有的定时任务其实都是一个线程在跑。
            this.localExecutor = Executors.newSingleThreadScheduledExecutor();
            //具体定时任务的实现类,下面会讲
            this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
        }
        if (this.triggerTasks != null) {
            for (TriggerTask task : triggerTasks) {
                this.scheduledFutures.add(this.taskScheduler.schedule(
                        task.getRunnable(), task.getTrigger()));
            }
        }
        if (this.cronTasks != null) {
            for (CronTask task : cronTasks) {
                        //遍历task集合,然后将返回的scheduledFuture加入到scheduledFutures集合中
                this.scheduledFutures.add(this.taskScheduler.schedule(
                        task.getRunnable(), task.getTrigger()));
            }
        }
        //为了篇幅,继续省略一些代码
}

这里我们看到了这个类怎么构造ScheduledFuture并加入到scheduledFutures集合中的。下面继续来看构造ScheduledFuture的关键类ConcurrentTaskScheduler

4. ConcurrentTaskScheduler类

首先我们先随便来看一个方法:

public ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period) {
        long initialDelay = startTime.getTime() - System.currentTimeMillis();
        try {
              //this.scheduledExecutor就是前面定义的那个单线程池
              //errorHandlingTask(task, true) 是对任务进行封装,如果任务抛出异常,我们可以按自己选择的方式来处理异常。第二个参数为true表示就算发生异常也会继续执行下一次任务。
            return this.scheduledExecutor.scheduleAtFixedRate(
                    errorHandlingTask(task, true), initialDelay, period, TimeUnit.MILLISECONDS);
        }
        catch (RejectedExecutionException ex) {
            throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);
        }
    }

这里可以看出,spring scheduler底层其实用的还是java的ScheduledExecutorService配置定时任务的。

再来看下最常用的cron方式配置的定时任务是怎么下发的:

public ScheduledFuture schedule(Runnable task, Trigger trigger) {
        try {
            ErrorHandler errorHandler =
                    (this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true));
            //将构造ScheduledFuture的权利交给了ReschedulingRunnable类
            return new ReschedulingRunnable(task, trigger, this.scheduledExecutor, errorHandler).schedule();
        }
        catch (RejectedExecutionException ex) {
            throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);
        }
    }

5. ReschedulingRunnable类

继续跟踪查看ReschedulingRunnable类的几个重要方法:

public ScheduledFuture schedule() {
        synchronized (this.triggerContextMonitor) {
            //根据cron表达式计算出下一次要执行的时间
            this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext);
            if (this.scheduledExecutionTime == null) {
                return null;
            }
            long initialDelay = this.scheduledExecutionTime.getTime() - System.currentTimeMillis();
              //用java自带的ScheduledExecutorService下发一个延迟任务(过xxxms后执行)
            this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);
            return this;
        }
    }

    @Override
    public void run() {
//真正开始跑
        Date actualExecutionTime = new Date();
        //执行父类的run方法
        super.run();
        Date completionTime = new Date();
        synchronized (this.triggerContextMonitor) {
            this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);
          //如果没有收到结束通知,继续看下一次什么时候执行
            if (!this.currentFuture.isCancelled()) {
                schedule();
            }
        }
    }

这是ReschedulingRunnable类的两个关键方法,schedule返回一个ScheduledFuture对象。他其实就是分析cron表示式后获取到下一次要执行的时间,然后交给ScheduledExecutorService去下发。
到执行的时间后,会执行run方法。run方法执行到后面,会继续调用schedule()方法。然后再计算下次执行的时候,然后下发。这两个方法一直循环,就构成了一个定时任务。

6.1将任务调度器缓存多线程

前面我们可以看到,这里的调度任务是多线程执行的,spring也提供了将单线程换成多线程执行的地方。我们是先看源码,还是ScheduledAnnotationBeanPostProcessor这个类。

public void onApplicationEvent(ContextRefreshedEvent event) {
        if (event.getApplicationContext() != this.applicationContext) {
            return;
        }
 //获取所有我们自己定义的SchedulingConfigurer接口实现类
        Map<String, SchedulingConfigurer> configurers =
                this.applicationContext.getBeansOfType(SchedulingConfigurer.class);

        if (this.scheduler != null) {
            this.registrar.setScheduler(this.scheduler);
        }
 //这里会将ScheduledTaskRegistrar传入这个方法,这样我们可以定义一个类,继承SchedulingConfigurer接口,到这里拿到ScheduledTaskRegistrar就可以修改线程池的实现了
        for (SchedulingConfigurer configurer : configurers.values()) {
            configurer.configureTasks(this.registrar);
        }
//后面省略
    }

spring的生命周期中,会自动调用onApplicationEvent这个方法。接着拿到SchedulingConfigurer的所有实现类,调用configureTasks方法,同时还会传入ScheduledTaskRegistrar对象。这个ScheduledTaskRegistrar实例是spring实现任务调度的关键所在!!!所以只要我们修改一下它的线程池实现(默认是单线程池),就可以将任务调度变成多线程了。下面上代码:

@Component
public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(10);
        threadPoolTaskScheduler.initialize();

        taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
        taskRegistrar.getScheduler().schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        }, new CronTrigger("0 0/5 * * * ?"));
    }
}

这里将调度的线程池调到了10个线程。

6.2 手动注入ThreadPoolTaskScheduler来实现多线程

<task:scheduler id="scheduler" pool-size="10"/>

在spring配置文件中加上上面的代码,spring启动的时候会往容器注入一个ThreadPoolTaskScheduler 实例。这个实例线程池大小配置为10。这样,spring启动的时候就会自动选择这个实例来作为ScheduledTaskRegistrar的TaskScheduler的实现。如果没在容器里面发现任何TaskScheduler实例,就会new一个单线程的线程池。

if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
            Map<String, ? super Object> schedulers = new HashMap<String, Object>();
            schedulers.putAll(applicationContext.getBeansOfType(TaskScheduler.class));
            schedulers.putAll(applicationContext.getBeansOfType(ScheduledExecutorService.class));
//没在容器中发现任何定时任务的线程池,就用默认的单线程池
            if (schedulers.size() == 0) {
                // do nothing -> fall back to default scheduler
            }
//发现了一个,就直接用它了
            else if (schedulers.size() == 1) {
                this.registrar.setScheduler(schedulers.values().iterator().next());
            }
//如果超过两个会报错哦!
            else if (schedulers.size() >= 2){
                throw new IllegalStateException(
                        "More than one TaskScheduler and/or ScheduledExecutorService  " +
                        "exist within the context. Remove all but one of the beans; or " +
                        "implement the SchedulingConfigurer interface and call " +
                        "ScheduledTaskRegistrar#setScheduler explicitly within the " +
                        "configureTasks() callback. Found the following beans: " + schedulers.keySet());
            }
        }

7. 结论

分析到这里我们其实可以得出一些结论:

  • spring scheduler底层使用的还是juc中的ScheduledExecutorService类。
  • 默认情况下,所有的定时任务都是放在一个线程跑的。也就是说,如果这个线程执行某个任务崩溃了或者执行某个任务的时候卡住了,那其他的定时任务也就都无法执行了。
  • 通过配置可以将单线程执行所有调度变成多线程执行所有调度任务。
  • TaskScheduler 接口是spring调度的核心接口,调度底层都用到了它(确切的说是它的一些实现类)。
  • 由于spring使用java**ScheduledExecutorService**配置定时任务的时候对任务做了封装,所以即使执行的任务发生了我们没有捕获的异常,也不会导致线程崩溃。因此,导致spring scheduler定时任务全部停止的原因很可能是因为执行某个定时任务的时候卡住了

总结到这里,再联想到最近往vos加了个定时任务,里面有用到httpclient发送http请求。猜想可能是这里卡住了。所以接着去了解了下apache的httpclient这个包。

二、httpclient 输入流未设置超时时间引起的阻塞

网上查了一下,httpclient的超时其实有两种,一种是连接超时(connect timeout),一种是读超时(socket timeout)。
httpclient的默认读时间是设置为0,也就是永远不超时的意思,这样就导致读的时候因为出现某个问题线程阻塞在读这里。通过jstack命令查看线程堆栈我们也可以确认线程确实阻塞在socket读那里了。

"org.springframework.jms.listener.DefaultMessageListenerContainer#7-1" prio=10 tid=0x00007f345127d800 nid=0x5b4f0 runnable [0x00007f34753d1000]  
   java.lang.Thread.State: RUNNABLE  
    at java.net.SocketInputStream.socketRead0(Native Method) 
    at java.net.SocketInputStream.read(SocketInputStream.java:150)  
    at java.net.SocketInputStream.read(SocketInputStream.java:121)  
    at org.apache.http.impl.io.AbstractSessionInputBuffer.fillBuffer(AbstractSessionInputBuffer.java:130)  
    at org.apache.http.impl.io.SocketInputBuffer.fillBuffer(SocketInputBuffer.java:127)  
    at org.apache.http.impl.io.AbstractSessionInputBuffer.readLine(AbstractSessionInputBuffer.java:233)  
    at org.apache.http.impl.io.ChunkedInputStream.getChunkSize(ChunkedInputStream.java:220)  
    at org.apache.http.impl.io.ChunkedInputStream.nextChunk(ChunkedInputStream.java:183)  
    at org.apache.http.impl.io.ChunkedInputStream.read(ChunkedInputStream.java:152)  
    at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:138)  
    at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:283)  
    at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:325)  
    at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:177)  
    - locked <0x000000070346ce70> (a java.io.InputStreamReader)  
    at java.io.InputStreamReader.read(InputStreamReader.java:184)  
    at java.io.Reader.read(Reader.java:140)  
    at org.apache.http.util.EntityUtils.toString(EntityUtils.java:161)  

我们可以通过设置读超时时间来解决这个问题:

HttpClient client = new HttpClient();
client.getHttpConnectionManager().getParams().setConnectionTimeout(5000);
client.getHttpConnectionManager().getParams().setSoTimeout(5000);

这里设置了5秒的连接超时时间和读超时时间,也就是说,阻塞5秒后如果还没有得到响应,就会放弃继续读,让线程可以往下执行。

猜你喜欢

转载自blog.csdn.net/u013332124/article/details/79480586