httpclient導致的spring schedule定時任務全部停止問題

前兩天vos線上服務遇到一個問題,定時任務突然全部停下來不跑了。看日志也沒發現什么明顯的異常輸出,加上比較忙,所以干脆直接使用重啟大法。沒想到過了兩天突然發現定時任務又全部停了,恰好正在趕需求,vos又是一個公司內部使用的系統,優先級較低,索性繼續重啟等有空了再來看。然后過了1天多定時任務?掛了。只能暫時放下手頭上的事看一下到底是什么導致的這個問題。

由于日志未輸出任何異常信息,所以要解決該問題只能靠分析代碼了。由于vos系統使用的定時任務框架是spring schduler,所以打算了解一下spring schduler的運行原理。

一、spring schduler 的運行原理

一般我們要使用schuduler,都會往spring的配置文件中加入下面這行。

<task:annotation-driven />

然后通過xml或者注解@Scheduled的方式配置具體的定時任務。下面我們圍繞注解的方式講解一下spring schduler是怎么解析和運行配置的定時任務的。

1. <task:annotation-driven />配置解析

spirng在解析配置文件的時候遇到<task:annotation-driven />的時候,會往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類。
  • 默認情況下,所有的定時任務都是放在一個線程跑的。也就是說,如果這個線程執行某個任務崩潰了或者執行某個任務的時候卡住了,那其他的定時任務也就都無法執行了。
  • 通過配置可以將單線程執行所有調度變成多線程執行所有調度任務。
  • 由于spring使用javaScheduledExecutorService配置定時任務的時候對任務做了封裝,所以即使執行的任務發生了我們沒有捕獲的異常,也不會導致線程崩潰。因此,導致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秒后如果還沒有得到響應,就會放棄繼續讀,讓線程可以往下執行。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容